/test/integration/hat-threads-run-every-frame.js

https://github.com/LLK/scratch-vm · JavaScript · 356 lines · 243 code · 49 blank · 64 comment · 6 complexity · 6add908876d4c22810ba8637a9e80e33 MD5 · raw file

  1. const path = require('path');
  2. const test = require('tap').test;
  3. const makeTestStorage = require('../fixtures/make-test-storage');
  4. const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
  5. const VirtualMachine = require('../../src/index');
  6. const Thread = require('../../src/engine/thread');
  7. const Runtime = require('../../src/engine/runtime');
  8. const execute = require('../../src/engine/execute.js');
  9. const projectUri = path.resolve(__dirname, '../fixtures/timer-greater-than-hat.sb2');
  10. const project = readFileToBuffer(projectUri);
  11. const checkIsHatThread = (t, vm, hatThread) => {
  12. t.equal(hatThread.stackClick, false);
  13. t.equal(hatThread.updateMonitor, false);
  14. const blockContainer = hatThread.target.blocks;
  15. const opcode = blockContainer.getOpcode(blockContainer.getBlock(hatThread.topBlock));
  16. t.assert(vm.runtime.getIsEdgeActivatedHat(opcode));
  17. };
  18. const checkIsStackClickThread = (t, vm, stackClickThread) => {
  19. t.equal(stackClickThread.stackClick, true);
  20. t.equal(stackClickThread.updateMonitor, false);
  21. };
  22. /**
  23. * timer-greater-than-hat.sb2 contains a single stack
  24. * when timer > -1
  25. * change color effect by 25
  26. * The intention is to make sure that the hat block condition is evaluated
  27. * on each frame.
  28. */
  29. test('edge activated hat thread runs once every frame', t => {
  30. const vm = new VirtualMachine();
  31. vm.attachStorage(makeTestStorage());
  32. // Start VM, load project, and run
  33. t.doesNotThrow(() => {
  34. // Note: don't run vm.start(), we handle calling _step() manually in this test
  35. vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
  36. vm.clear();
  37. vm.setCompatibilityMode(false);
  38. vm.setTurboMode(false);
  39. vm.loadProject(project).then(() => {
  40. t.equal(vm.runtime.threads.length, 0);
  41. vm.runtime._step();
  42. let threads = vm.runtime._lastStepDoneThreads;
  43. t.equal(vm.runtime.threads.length, 0);
  44. t.equal(threads.length, 1);
  45. checkIsHatThread(t, vm, threads[0]);
  46. t.assert(threads[0].status === Thread.STATUS_DONE);
  47. // Check that the hat thread is added again when another step is taken
  48. vm.runtime._step();
  49. threads = vm.runtime._lastStepDoneThreads;
  50. t.equal(vm.runtime.threads.length, 0);
  51. t.equal(threads.length, 1);
  52. checkIsHatThread(t, vm, threads[0]);
  53. t.assert(threads[0].status === Thread.STATUS_DONE);
  54. t.end();
  55. });
  56. });
  57. });
  58. /**
  59. * When a hat is added it should run in the next frame. Any block related
  60. * caching should be reset.
  61. */
  62. test('edge activated hat thread runs after being added to previously executed target', t => {
  63. const vm = new VirtualMachine();
  64. vm.attachStorage(makeTestStorage());
  65. // Start VM, load project, and run
  66. t.doesNotThrow(() => {
  67. // Note: don't run vm.start(), we handle calling _step() manually in this test
  68. vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
  69. vm.clear();
  70. vm.setCompatibilityMode(false);
  71. vm.setTurboMode(false);
  72. vm.loadProject(project).then(() => {
  73. t.equal(vm.runtime.threads.length, 0);
  74. vm.runtime._step();
  75. let threads = vm.runtime._lastStepDoneThreads;
  76. t.equal(vm.runtime.threads.length, 0);
  77. t.equal(threads.length, 1);
  78. checkIsHatThread(t, vm, threads[0]);
  79. t.assert(threads[0].status === Thread.STATUS_DONE);
  80. // Add a second hat that should create a second thread
  81. const hatBlock = threads[0].target.blocks.getBlock(threads[0].topBlock);
  82. threads[0].target.blocks.createBlock(Object.assign(
  83. {}, hatBlock, {id: 'hatblock2', next: null}
  84. ));
  85. // Check that the hat thread is added again when another step is taken
  86. vm.runtime._step();
  87. threads = vm.runtime._lastStepDoneThreads;
  88. t.equal(vm.runtime.threads.length, 0);
  89. t.equal(threads.length, 2);
  90. checkIsHatThread(t, vm, threads[0]);
  91. checkIsHatThread(t, vm, threads[1]);
  92. t.assert(threads[0].status === Thread.STATUS_DONE);
  93. t.assert(threads[1].status === Thread.STATUS_DONE);
  94. t.end();
  95. });
  96. });
  97. });
  98. /**
  99. * If the hat doesn't finish evaluating within one frame, it shouldn't be added again
  100. * on the next frame. (We skip execution by setting the step time to 0)
  101. */
  102. test('edge activated hat thread not added twice', t => {
  103. const vm = new VirtualMachine();
  104. vm.attachStorage(makeTestStorage());
  105. // Start VM, load project, and run
  106. t.doesNotThrow(() => {
  107. // Note: don't run vm.start(), we handle calling _step() manually in this test
  108. vm.runtime.currentStepTime = 0;
  109. vm.clear();
  110. vm.setCompatibilityMode(false);
  111. vm.setTurboMode(false);
  112. vm.loadProject(project).then(() => {
  113. t.equal(vm.runtime.threads.length, 0);
  114. vm.runtime._step();
  115. let doneThreads = vm.runtime._lastStepDoneThreads;
  116. t.equal(vm.runtime.threads.length, 1);
  117. t.equal(doneThreads.length, 0);
  118. const prevThread = vm.runtime.threads[0];
  119. checkIsHatThread(t, vm, vm.runtime.threads[0]);
  120. t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
  121. // Check that no new threads are added when another step is taken
  122. vm.runtime._step();
  123. doneThreads = vm.runtime._lastStepDoneThreads;
  124. // There should now be one done hat thread and one new hat thread to run
  125. t.equal(vm.runtime.threads.length, 1);
  126. t.equal(doneThreads.length, 0);
  127. checkIsHatThread(t, vm, vm.runtime.threads[0]);
  128. t.assert(vm.runtime.threads[0] === prevThread);
  129. t.end();
  130. });
  131. });
  132. });
  133. /**
  134. * Duplicating a sprite should also track duplicated edge activated hat in
  135. * runtime's _edgeActivatedHatValues map.
  136. */
  137. test('edge activated hat should trigger for both sprites when sprite is duplicated', t => {
  138. // Project that is similar to timer-greater-than-hat.sb2, but has code on the sprite so that
  139. // the sprite can be duplicated
  140. const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3');
  141. const projectWithSprite = readFileToBuffer(projectWithSpriteUri);
  142. const vm = new VirtualMachine();
  143. vm.attachStorage(makeTestStorage());
  144. // Start VM, load project, and run
  145. t.doesNotThrow(() => {
  146. // Note: don't run vm.start(), we handle calling _step() manually in this test
  147. vm.runtime.currentStepTime = 0;
  148. vm.clear();
  149. vm.setCompatibilityMode(false);
  150. vm.setTurboMode(false);
  151. vm.loadProject(projectWithSprite).then(() => {
  152. t.equal(vm.runtime.threads.length, 0);
  153. vm.runtime._step();
  154. t.equal(vm.runtime.threads.length, 1);
  155. checkIsHatThread(t, vm, vm.runtime.threads[0]);
  156. t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
  157. let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
  158. val + Object.keys(target._edgeActivatedHatValues).length, 0);
  159. t.equal(numTargetEdgeHats, 1);
  160. vm.duplicateSprite(vm.runtime.targets[1].id).then(() => {
  161. vm.runtime._step();
  162. // Check that the runtime's _edgeActivatedHatValues object has two separate keys
  163. // after execute is run on each thread
  164. numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
  165. val + Object.keys(target._edgeActivatedHatValues).length, 0);
  166. t.equal(numTargetEdgeHats, 2);
  167. t.end();
  168. });
  169. });
  170. });
  171. });
  172. /**
  173. * Cloning a sprite should also track cloned edge activated hat separately
  174. * runtime's _edgeActivatedHatValues map.
  175. */
  176. test('edge activated hat should trigger for both sprites when sprite is cloned', t => {
  177. // Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that
  178. // the sprite can be duplicated
  179. const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3');
  180. const projectWithSprite = readFileToBuffer(projectWithSpriteUri);
  181. const vm = new VirtualMachine();
  182. vm.attachStorage(makeTestStorage());
  183. // Start VM, load project, and run
  184. t.doesNotThrow(() => {
  185. // Note: don't run vm.start(), we handle calling _step() manually in this test
  186. vm.runtime.currentStepTime = 0;
  187. vm.clear();
  188. vm.setCompatibilityMode(false);
  189. vm.setTurboMode(false);
  190. vm.loadProject(projectWithSprite).then(() => {
  191. t.equal(vm.runtime.threads.length, 0);
  192. vm.runtime._step();
  193. t.equal(vm.runtime.threads.length, 1);
  194. checkIsHatThread(t, vm, vm.runtime.threads[0]);
  195. t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
  196. // Run execute on the thread to populate the runtime's
  197. // _edgeActivatedHatValues object
  198. execute(vm.runtime.sequencer, vm.runtime.threads[0]);
  199. let numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
  200. val + Object.keys(target._edgeActivatedHatValues).length, 0);
  201. t.equal(numTargetEdgeHats, 1);
  202. const cloneTarget = vm.runtime.targets[1].makeClone();
  203. vm.runtime.addTarget(cloneTarget);
  204. vm.runtime._step();
  205. // Check that the runtime's _edgeActivatedHatValues object has two separate keys
  206. // after execute is run on each thread
  207. vm.runtime.threads.forEach(thread => execute(vm.runtime.sequencer, thread));
  208. numTargetEdgeHats = vm.runtime.targets.reduce((val, target) =>
  209. val + Object.keys(target._edgeActivatedHatValues).length, 0);
  210. t.equal(numTargetEdgeHats, 2);
  211. t.end();
  212. });
  213. });
  214. });
  215. /**
  216. * When adding a stack click thread first, make sure that the edge activated hat thread and
  217. * the stack click thread are both pushed and run (despite having the same top block)
  218. */
  219. test('edge activated hat thread does not interrupt stack click thread', t => {
  220. const vm = new VirtualMachine();
  221. vm.attachStorage(makeTestStorage());
  222. // Start VM, load project, and run
  223. t.doesNotThrow(() => {
  224. // Note: don't run vm.start(), we handle calling _step() manually in this test
  225. vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
  226. vm.clear();
  227. vm.setCompatibilityMode(false);
  228. vm.setTurboMode(false);
  229. vm.loadProject(project).then(() => {
  230. t.equal(vm.runtime.threads.length, 0);
  231. vm.runtime._step();
  232. let doneThreads = vm.runtime._lastStepDoneThreads;
  233. t.equal(vm.runtime.threads.length, 0);
  234. t.equal(doneThreads.length, 1);
  235. checkIsHatThread(t, vm, doneThreads[0]);
  236. t.assert(doneThreads[0].status === Thread.STATUS_DONE);
  237. // Add stack click thread on this hat
  238. vm.runtime.toggleScript(doneThreads[0].topBlock, {stackClick: true});
  239. // Check that the hat thread is added again when another step is taken
  240. vm.runtime._step();
  241. doneThreads = vm.runtime._lastStepDoneThreads;
  242. t.equal(vm.runtime.threads.length, 0);
  243. t.equal(doneThreads.length, 2);
  244. let hatThread;
  245. let stackClickThread;
  246. if (doneThreads[0].stackClick) {
  247. stackClickThread = doneThreads[0];
  248. hatThread = doneThreads[1];
  249. } else {
  250. stackClickThread = doneThreads[1];
  251. hatThread = doneThreads[0];
  252. }
  253. checkIsHatThread(t, vm, hatThread);
  254. checkIsStackClickThread(t, vm, stackClickThread);
  255. t.assert(doneThreads[0].status === Thread.STATUS_DONE);
  256. t.assert(doneThreads[1].status === Thread.STATUS_DONE);
  257. t.end();
  258. });
  259. });
  260. });
  261. /**
  262. * When adding the hat thread first, make sure that the edge activated hat thread and
  263. * the stack click thread are both pushed and run (despite having the same top block)
  264. */
  265. test('edge activated hat thread does not interrupt stack click thread', t => {
  266. const vm = new VirtualMachine();
  267. vm.attachStorage(makeTestStorage());
  268. // Start VM, load project, and run
  269. t.doesNotThrow(() => {
  270. // Note: don't run vm.start(), we handle calling _step() manually in this test
  271. vm.runtime.currentStepTime = 0;
  272. vm.clear();
  273. vm.setCompatibilityMode(false);
  274. vm.setTurboMode(false);
  275. vm.loadProject(project).then(() => {
  276. t.equal(vm.runtime.threads.length, 0);
  277. vm.runtime._step();
  278. let doneThreads = vm.runtime._lastStepDoneThreads;
  279. t.equal(vm.runtime.threads.length, 1);
  280. t.equal(doneThreads.length, 0);
  281. checkIsHatThread(t, vm, vm.runtime.threads[0]);
  282. t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING);
  283. vm.runtime.currentStepTime = Runtime.THREAD_STEP_INTERVAL;
  284. // Add stack click thread on this hat
  285. vm.runtime.toggleScript(vm.runtime.threads[0].topBlock, {stackClick: true});
  286. // Check that the hat thread is added again when another step is taken
  287. vm.runtime._step();
  288. doneThreads = vm.runtime._lastStepDoneThreads;
  289. t.equal(vm.runtime.threads.length, 0);
  290. t.equal(doneThreads.length, 2);
  291. let hatThread;
  292. let stackClickThread;
  293. if (doneThreads[0].stackClick) {
  294. stackClickThread = doneThreads[0];
  295. hatThread = doneThreads[1];
  296. } else {
  297. stackClickThread = doneThreads[1];
  298. hatThread = doneThreads[0];
  299. }
  300. checkIsHatThread(t, vm, hatThread);
  301. checkIsStackClickThread(t, vm, stackClickThread);
  302. t.assert(doneThreads[0].status === Thread.STATUS_DONE);
  303. t.assert(doneThreads[1].status === Thread.STATUS_DONE);
  304. t.end();
  305. });
  306. });
  307. });