Compare commits

...

21 Commits

Author SHA1 Message Date
greenkeeper[bot]
097daeec28 chore(package): update scratch-vm to version 0.2.0-prerelease.20200519195348
Closes #560
2020-05-19 20:10:34 +00:00
adroitwhiz
d73aeb1ac1 Merge pull request #598 from adroitwhiz/cpu-gpu-integration-tests
Run integration tests on both the CPU and GPU (again)
2020-05-12 16:24:10 -04:00
Christopher Willis-Ford
298200f2ee Make touching-color test more robust against GPU imprecision
Previously, the `color-touching-tests.sb2` test "touches a color that
doesn't actually exist right now" would use a sprite with ghost 50,
blended against another sprite, to create the color that "doesn't
actually exist" when the query sprite is skipped. Unfortunately the
blend result was near a bit-boundary and, depending on the specific
hardware used, that test could fail on the GPU. When the renderer uses
the CPU path this test works fine, though, so the existing problem went
unnoticed.

To fix the problem I changed the project to use ghost 30 instead, which
results in a color that is less near a bit boundary and is therefore
less likely to fail on specific hardware.

As an example of what was happening: the `touching color` block was
checking for `RGB(127,101,216)` with a mask of `RGB(0xF8,0xF8,0xF0)`. On
the CPU it would find `RGB(120,99,215)`, which is in range, but on some
GPUs the closest color it could find was `RGB(119,98,215)` which
mismatches on all four of the least significant bits -- one of which is
enabled in the mask.
2020-05-12 15:52:51 -04:00
adroitwhiz
b9411bf51e Skip "doesn't touch say bubble" test
There are some compatibility concerns to be worked out before the
corresponding bug can be fixed.
2020-05-12 15:52:51 -04:00
adroitwhiz
3513cd77ae Adapt f9428ee to Playwright 2020-05-12 15:52:51 -04:00
adroitwhiz
d8c9f339f7 Merge pull request #576 from adroitwhiz/fix-ghost-touching-color
Don't apply ghost effect to "color is touching color" drawable
2020-05-11 16:29:17 -04:00
adroitwhiz
aaffc77b23 Clarify isTouchingColor ghost comment 2020-05-11 16:07:05 -04:00
adroitwhiz
d867e62c74 Don't apply ghost to "touching color" drawables 2020-05-11 16:07:05 -04:00
adroitwhiz
a7bec3a958 Merge pull request #604 from adroitwhiz/playwright
Playwright: the re-wright
2020-05-11 15:50:57 -04:00
adroitwhiz
0ce50b6cd6 log error.message 2020-05-11 15:38:11 -04:00
adroitwhiz
562e535a13 recache node_modules 2020-05-11 15:02:01 -04:00
adroitwhiz
42d3f6c012 uncache node_modules 2020-05-11 14:58:47 -04:00
adroitwhiz
9c01f364d4 Catch promise errors and exit with code 1 2020-05-11 14:43:54 -04:00
adroitwhiz
5d085f678f Add cached Playwright directory to Travis cache 2020-05-11 14:43:54 -04:00
adroitwhiz
7df536f492 Bump Node to 10 2020-05-11 14:43:54 -04:00
adroitwhiz
a41bcafac7 Use Playwright instead of Puppeteer 2020-05-11 14:43:54 -04:00
adroitwhiz
5566a600ba reference "wait for SVG skins to load" issue 2020-05-11 14:43:54 -04:00
adroitwhiz
34072d2f53 Fix file input in tests 2020-05-11 14:43:54 -04:00
adroitwhiz
fdd02a6bd6 download Chromium only when tests are run 2020-05-11 14:43:54 -04:00
adroitwhiz
ec141ff76d Replace Chromeless with Puppeteer 2020-05-11 14:43:54 -04:00
adroitwhiz
3f68e18b2e Merge pull request #603 from LLK/revert-597-playwright
Revert "Switch from Chromeless to Playwright for tests"
2020-05-08 16:40:47 -04:00
12 changed files with 177 additions and 134 deletions

View File

@@ -1,14 +1,10 @@
language: node_js
dist: trusty
addons:
chrome: stable
node_js:
- 8
- 10
- node
env:
- NODE_ENV=production
before_install:
- google-chrome-stable --headless --no-sandbox --remote-debugging-port=9222 &
install:
- npm --production=false install
- npm --production=false update
@@ -16,6 +12,7 @@ sudo: false
cache:
directories:
- node_modules
- ~/.cache/ms-playwright
jobs:
include:
- stage: test
@@ -24,7 +21,7 @@ jobs:
- npm run docs
- npm run tap
- stage: deploy
node_js: 8
node_js: 10
script: npm run build
before_deploy:
- VPKG=$($(npm bin)/json -f package.json version)

View File

@@ -29,7 +29,6 @@
"babel-loader": "^7.1.4",
"babel-polyfill": "^6.22.0",
"babel-preset-env": "^1.6.1",
"chromeless": "^1.5.1",
"copy-webpack-plugin": "^4.5.1",
"docdash": "^0.4.0",
"eslint": "^4.6.1",
@@ -37,7 +36,8 @@
"gh-pages": "^1.0.0",
"jsdoc": "^3.5.5",
"json": "^9.0.4",
"scratch-vm": "0.2.0-prerelease.20191227164934",
"playwright-chromium": "^1.0.1",
"scratch-vm": "0.2.0-prerelease.20200519195348",
"tap": "^11.0.0",
"travis-after-all": "^1.4.4",
"uglifyjs-webpack-plugin": "^1.2.5",

View File

@@ -679,9 +679,10 @@ class Drawable {
* @param {twgl.v3} vec The scratch space [x,y] vector
* @param {Drawable} drawable The drawable to sample the texture from
* @param {Uint8ClampedArray} dst The "color4b" representation of the texture at point.
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
* @returns {Uint8ClampedArray} The dst object filled with the color4b
*/
static sampleColor4b (vec, drawable, dst) {
static sampleColor4b (vec, drawable, dst, effectMask) {
const localPosition = getLocalPosition(drawable, vec);
if (localPosition[0] < 0 || localPosition[1] < 0 ||
localPosition[0] > 1 || localPosition[1] > 1) {
@@ -698,7 +699,7 @@ class Drawable {
// : drawable.skin._silhouette.colorAtLinear(localPosition, dst);
if (drawable.enabledEffects === 0) return textColor;
return EffectTransform.transformColor(drawable, textColor);
return EffectTransform.transformColor(drawable, textColor, effectMask);
}
}

View File

@@ -118,16 +118,17 @@ class EffectTransform {
* Ghost and Color and Brightness effects.
* @param {Drawable} drawable The drawable to get uniforms from.
* @param {Uint8ClampedArray} inOutColor The color to transform.
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
* @returns {Uint8ClampedArray} dst filled with the transformed color
*/
static transformColor (drawable, inOutColor) {
static transformColor (drawable, inOutColor, effectMask) {
// If the color is fully transparent, don't bother attempting any transformations.
if (inOutColor[3] === 0) {
return inOutColor;
}
const effects = drawable.enabledEffects;
let effects = drawable.enabledEffects;
if (typeof effectMask === 'number') effects &= effectMask;
const uniforms = drawable.getUniforms();
const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;

View File

@@ -763,6 +763,9 @@ class RenderWebGL extends EventEmitter {
const color = __touchingColor;
const hasMask = Boolean(mask3b);
// Masked drawable ignores ghost effect
const effectMask = ~ShaderManager.EFFECT_INFO.ghost.mask;
// Scratch Space - +y is top
for (let y = bounds.bottom; y <= bounds.top; y++) {
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) {
@@ -773,7 +776,7 @@ class RenderWebGL extends EventEmitter {
point[0] = x;
// if we use a mask, check our sample color...
if (hasMask ?
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
maskMatches(Drawable.sampleColor4b(point, drawable, color, effectMask), mask3b) :
drawable.isTouching(point)) {
RenderWebGL.sampleColor3b(point, candidates, color);
if (debugCanvasContext) {

54
test/helper/page-util.js Normal file
View File

@@ -0,0 +1,54 @@
/* global window, VirtualMachine, ScratchStorage, ScratchSVGRenderer */
/* eslint-env browser */
// Wait for all SVG skins to be loaded.
// TODO: this is extremely janky and should be removed once vm.loadProject waits for SVG skins to load
// https://github.com/LLK/scratch-render/issues/563
window.waitForSVGSkinLoad = renderer => new Promise(resolve => {
// eslint-disable-next-line prefer-const
let interval;
const waitInner = () => {
let numSVGSkins = 0;
let numLoadedSVGSkins = 0;
for (const skin of renderer._allSkins) {
if (skin.constructor.name !== 'SVGSkin') continue;
numSVGSkins++;
if (skin._svgRenderer.loaded) numLoadedSVGSkins++;
}
if (numSVGSkins === numLoadedSVGSkins) {
clearInterval(interval);
resolve();
}
};
interval = setInterval(waitInner, 1);
});
window.loadFileInputIntoVM = (fileInput, vm, render) => {
const reader = new FileReader();
return new Promise(resolve => {
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => window.waitForSVGSkinLoad(render))
.then(() => {
resolve();
});
};
reader.readAsArrayBuffer(fileInput.files[0]);
});
};
window.initVM = render => {
const vm = new VirtualMachine();
const storage = new ScratchStorage();
vm.attachStorage(storage);
vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
return vm;
};

View File

@@ -2,6 +2,7 @@
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
<script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script>
<script src="../helper/page-util.js"></script>
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
<script src="../../dist/web/scratch-render.js"></script>
@@ -17,38 +18,18 @@
window.devicePixelRatio = 1;
const gpuCanvas = document.getElementById('test');
var render = new ScratchRender(gpuCanvas);
var vm = new VirtualMachine();
var storage = new ScratchStorage();
var vm = initVM(render);
vm.attachStorage(storage);
vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
document.getElementById('file').addEventListener('click', e => {
document.body.removeChild(document.getElementById('loaded'));
});
document.getElementById('file').addEventListener('change', e => {
const reader = new FileReader();
const thisFileInput = e.target;
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => {
// we add a `#loaded` div to our document, the integration suite
// waits for that element to show up to assume the vm is ready
// to play!
const div = document.createElement('div');
div.id='loaded';
document.body.appendChild(div);
vm.greenFlag();
setTimeout(() => {
renderCpu();
}, 1000);
});
};
reader.readAsArrayBuffer(thisFileInput.files[0]);
const fileInput = document.getElementById('file');
const loadFile = loadFileInputIntoVM.bind(null, fileInput, vm, render);
fileInput.addEventListener('change', e => {
loadFile()
.then(() => {
vm.greenFlag();
setTimeout(() => {
renderCpu();
}, 1000);
});
});
const cpuCanvas = document.getElementById('cpu');

View File

@@ -2,6 +2,7 @@
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
<script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script>
<script src="../helper/page-util.js"></script>
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
<script src="../../dist/web/scratch-render.js"></script>
@@ -15,39 +16,13 @@
var canvas = document.getElementById('test');
var render = new ScratchRender(canvas);
var vm = new VirtualMachine();
var storage = new ScratchStorage();
var vm = initVM(render);
var mockMouse = data => vm.runtime.postIOData('mouse', {
canvasWidth: canvas.width,
canvasHeight: canvas.height,
...data,
});
vm.attachStorage(storage);
vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
document.getElementById('file').addEventListener('click', e => {
document.body.removeChild(document.getElementById('loaded'));
});
document.getElementById('file').addEventListener('change', e => {
const reader = new FileReader();
const thisFileInput = e.target;
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => {
// we add a `#loaded` div to our document, the integration suite
// waits for that element to show up to assume the vm is ready
// to play!
const div = document.createElement('div');
div.id='loaded';
document.body.appendChild(div);
});
};
reader.readAsArrayBuffer(thisFileInput.files[0]);
});
const loadFile = loadFileInputIntoVM.bind(null, document.getElementById('file'), vm, render);
</script>
</body>

View File

@@ -1,29 +1,34 @@
/* global vm, render, Promise */
const {Chromeless} = require('chromeless');
const {chromium} = require('playwright-chromium');
const test = require('tap').test;
const path = require('path');
const chromeless = new Chromeless();
const indexHTML = path.resolve(__dirname, 'index.html');
const testDir = (...args) => path.resolve(__dirname, 'pick-tests', ...args);
const runFile = (file, action, script) =>
const runFile = async (file, action, page, script) => {
// start each test by going to the index.html, and loading the scratch file
chromeless.goto(`file://${indexHTML}`)
.setFileInput('#file', testDir(file))
// the index.html handler for file input will add a #loaded element when it
// finishes.
.wait('#loaded')
.evaluate(`function () {return (${script})(${action});}`)
;
await page.goto(`file://${indexHTML}`);
const fileInput = await page.$('#file');
await fileInput.setInputFiles(testDir(file));
await page.evaluate(() =>
// `loadFile` is defined on the page itself.
// eslint-disable-next-line no-undef
loadFile()
);
return page.evaluate(`(function () {return (${script})(${action});})()`);
};
// immediately invoked async function to let us wait for each test to finish before starting the next.
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const testOperation = async function (name, action, expect) {
await test(name, async t => {
const results = await runFile('test-mouse-touch.sb2', action, boundAction => {
const results = await runFile('test-mouse-touch.sb2', action, page, boundAction => {
vm.greenFlag();
const sendResults = [];
@@ -97,5 +102,10 @@ const runFile = (file, action, script) =>
}
// close the browser window we used
await chromeless.end();
})();
await browser.close();
})().catch(err => {
// Handle promise rejections by exiting with a nonzero code to ensure that tests don't erroneously pass
// eslint-disable-next-line no-console
console.error(err.message);
process.exit(1);
});

View File

@@ -1,55 +1,15 @@
/* global vm, Promise */
const {Chromeless} = require('chromeless');
const {chromium} = require('playwright-chromium');
const test = require('tap').test;
const path = require('path');
const fs = require('fs');
const chromeless = new Chromeless();
const allGpuModes = ['ForceCPU', 'ForceGPU', 'Automatic'];
const indexHTML = path.resolve(__dirname, 'index.html');
const testDir = (...args) => path.resolve(__dirname, 'scratch-tests', ...args);
const testFile = file => test(file, async t => {
// start each test by going to the index.html, and loading the scratch file
const says = await chromeless.goto(`file://${indexHTML}`)
.setFileInput('#file', testDir(file))
// the index.html handler for file input will add a #loaded element when it
// finishes.
.wait('#loaded')
.evaluate(() => {
// This function is run INSIDE the integration chrome browser via some
// injection and .toString() magic. We can return some "simple data"
// back across as a promise, so we will just log all the says that happen
// for parsing after.
// this becomes the `says` in the outer scope
const messages = [];
const TIMEOUT = 5000;
vm.runtime.on('SAY', (_, __, message) => {
messages.push(message);
});
vm.greenFlag();
const startTime = Date.now();
return Promise.resolve()
.then(async () => {
// waiting for all threads to complete, then we return
while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) {
if ((Date.now() - startTime) >= TIMEOUT) {
// if we push the message after end, the failure from tap is not very useful:
// "not ok test after end() was called"
messages.unshift(`fail Threads still running after ${TIMEOUT}ms`);
break;
}
await new Promise(resolve => setTimeout(resolve, 50));
}
return messages;
});
});
const checkOneGpuMode = (t, says) => {
// Map string messages to tap reporting methods. This will be used
// with events from scratch's runtime emitted on block instructions.
let didPlan = false;
@@ -99,17 +59,78 @@ const testFile = file => test(file, async t => {
t.fail('did not say "end"');
t.end();
}
});
};
const testFile = async (file, page) => {
// start each test by going to the index.html, and loading the scratch file
await page.goto(`file://${indexHTML}`);
const fileInput = await page.$('#file');
await fileInput.setInputFiles(testDir(file));
await page.evaluate(() =>
// `loadFile` is defined on the page itself.
// eslint-disable-next-line no-undef
loadFile()
);
const says = await page.evaluate(async useGpuModes => {
// This function is run INSIDE the integration chrome browser via some
// injection and .toString() magic. We can return some "simple data"
// back across as a promise, so we will just log all the says that happen
// for parsing after.
// this becomes the `says` in the outer scope
const allMessages = {};
const TIMEOUT = 5000;
vm.runtime.on('SAY', (_, __, message) => {
const messages = allMessages[vm.renderer._useGpuMode];
messages.push(message);
});
for (const useGpuMode of useGpuModes) {
const messages = allMessages[useGpuMode] = [];
vm.renderer.setUseGpuMode(useGpuMode);
vm.greenFlag();
const startTime = Date.now();
// wait for all threads to complete before moving on to the next mode
while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) {
if ((Date.now() - startTime) >= TIMEOUT) {
// if we push the message after end, the failure from tap is not very useful:
// "not ok test after end() was called"
messages.unshift(`fail Threads still running after ${TIMEOUT}ms`);
break;
}
await new Promise(resolve => setTimeout(resolve, 50));
}
}
return allMessages;
}, allGpuModes);
for (const gpuMode of allGpuModes) {
test(`File: ${file}, GPU Mode: ${gpuMode}`, t => checkOneGpuMode(t, says[gpuMode]));
}
};
// immediately invoked async function to let us wait for each test to finish before starting the next.
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const files = fs.readdirSync(testDir())
.filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3'));
for (const file of files) {
await testFile(file);
await testFile(file, page);
}
// close the browser window we used
await chromeless.end();
})();
await browser.close();
})().catch(err => {
// Handle promise rejections by exiting with a nonzero code to ensure that tests don't erroneously pass
// eslint-disable-next-line no-console
console.error(err.message);
process.exit(1);
});