Add Correctness tests for CanvasKit

Also make a CPU only and GPU only build (although
the latter still has a lot of CPU logic).

Bug: skia:
Change-Id: I857c2300021c2adb5344865c28e4ad3e8d332954
Reviewed-on: https://skia-review.googlesource.com/c/162022
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/experimental/canvaskit/Makefile b/experimental/canvaskit/Makefile
index 65764fb..f63395d 100644
--- a/experimental/canvaskit/Makefile
+++ b/experimental/canvaskit/Makefile
@@ -1,25 +1,30 @@
 clean:
 	rm -rf ../../out/canvaskit_wasm
-	rm -rf ./canvas-kit/bin
+	rm -rf ./canvaskit/bin
 	$(MAKE) release
 
 release:
 	# Does an incremental build where possible.
 	./compile.sh
-	mkdir -p ./canvas-kit/bin
-	cp ../../out/canvaskit_wasm/canvaskit.js   ./canvas-kit/bin
-	cp ../../out/canvaskit_wasm/canvaskit.wasm ./canvas-kit/bin
+	mkdir -p ./canvaskit/bin
+	cp ../../out/canvaskit_wasm/canvaskit.js   ./canvaskit/bin
+	cp ../../out/canvaskit_wasm/canvaskit.wasm ./canvaskit/bin
 
 debug:
 	# Does an incremental build where possible.
 	./compile.sh debug
-	mkdir -p ./canvas-kit/bin
-	cp ../../out/canvaskit_wasm/canvaskit.js   ./canvas-kit/bin
-	cp ../../out/canvaskit_wasm/canvaskit.wasm ./canvas-kit/bin
+	mkdir -p ./canvaskit/bin
+	cp ../../out/canvaskit_wasm_debug/canvaskit.js   ./canvaskit/bin
+	cp ../../out/canvaskit_wasm_debug/canvaskit.wasm ./canvaskit/bin
 
 local-example:
-	rm -rf node_modules/canvas-kit
+	rm -rf node_modules/canvaskit
 	mkdir -p node_modules
-	ln -s -T ../canvas-kit  node_modules/canvas-kit
-	echo "Go check out http://localhost:8000/canvas-kit/example.html"
+	ln -s -T ../canvaskit node_modules/canvaskit
+	echo "Go check out http://localhost:8000/canvaskit/example.html"
 	python serve.py
+
+test-continuous:
+	echo "Assuming npm install has been run by user"
+	echo "Also assuming make debug or release has also been run by a user (if needed)"
+	npx karma start ./karma.conf.js --no-single-run --watch-poll
\ No newline at end of file
diff --git a/experimental/canvaskit/canvas-kit/.gitignore b/experimental/canvaskit/canvas-kit/.gitignore
deleted file mode 100644
index 6dd29b7..0000000
--- a/experimental/canvaskit/canvas-kit/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-bin/
\ No newline at end of file
diff --git a/experimental/canvaskit/canvaskit/.gitignore b/experimental/canvaskit/canvaskit/.gitignore
new file mode 100644
index 0000000..d3c3499
--- /dev/null
+++ b/experimental/canvaskit/canvaskit/.gitignore
@@ -0,0 +1,3 @@
+bin/
+package-lock.json
+node_modules/
\ No newline at end of file
diff --git a/experimental/canvaskit/canvas-kit/cpu_example.html b/experimental/canvaskit/canvaskit/cpu_example.html
similarity index 100%
rename from experimental/canvaskit/canvas-kit/cpu_example.html
rename to experimental/canvaskit/canvaskit/cpu_example.html
diff --git a/experimental/canvaskit/canvas-kit/example.html b/experimental/canvaskit/canvaskit/example.html
similarity index 100%
rename from experimental/canvaskit/canvas-kit/example.html
rename to experimental/canvaskit/canvaskit/example.html
diff --git a/experimental/canvaskit/canvas-kit/package.json b/experimental/canvaskit/canvaskit/package.json
similarity index 100%
rename from experimental/canvaskit/canvas-kit/package.json
rename to experimental/canvaskit/canvaskit/package.json
diff --git a/experimental/canvaskit/canvaskit_bindings.cpp b/experimental/canvaskit/canvaskit_bindings.cpp
index 82e65c7..fcf0042 100644
--- a/experimental/canvaskit/canvaskit_bindings.cpp
+++ b/experimental/canvaskit/canvaskit_bindings.cpp
@@ -13,7 +13,6 @@
 #endif
 
 #include "SkCanvas.h"
-#include "SkCanvas.h"
 #include "SkDashPathEffect.h"
 #include "SkCornerPathEffect.h"
 #include "SkDiscretePathEffect.h"
@@ -211,10 +210,12 @@
     function("_getWebGLSurface", &getWebGLSurface, allow_raw_pointers());
     function("currentContext", &emscripten_webgl_get_current_context);
     function("setCurrentContext", &emscripten_webgl_make_context_current);
-#endif
+    constant("gpu", true);
+#else
     function("_getRasterN32PremulSurface", optional_override([](int width, int height)->sk_sp<SkSurface> {
         return SkSurface::MakeRasterN32Premul(width, height, nullptr);
     }), allow_raw_pointers());
+#endif
     function("MakeSkCornerPathEffect", &SkCornerPathEffect::Make, allow_raw_pointers());
     function("MakeSkDiscretePathEffect", &SkDiscretePathEffect::Make, allow_raw_pointers());
     // Won't be called directly, there's a JS helper to deal with typed arrays.
@@ -236,6 +237,9 @@
         .function("drawPath", &SkCanvas::drawPath)
         .function("drawRect", &SkCanvas::drawRect)
         .function("drawText", optional_override([](SkCanvas& self, std::string text, SkScalar x, SkScalar y, const SkPaint& p) {
+            // TODO(kjlubick): This does not work well for non-ascii
+            // Need to maybe add a helper in interface.js that supports UTF-8
+            // Otherwise, go with std::wstring and set UTF-32 encoding.
             self.drawText(text.c_str(), text.length(), x, y, p);
         }))
         .function("flush", &SkCanvas::flush)
@@ -271,7 +275,6 @@
     class_<SkPathEffect>("SkPathEffect")
         .smart_ptr<sk_sp<SkPathEffect>>("sk_sp<SkPathEffect>");
 
-    //TODO make these chainable like PathKit
     class_<SkPath>("SkPath")
         .constructor<>()
         .constructor<const SkPath&>()
@@ -297,6 +300,7 @@
         .smart_ptr<sk_sp<SkSurface>>("sk_sp<SkSurface>")
         .function("width", &SkSurface::width)
         .function("height", &SkSurface::height)
+        .function("_flush", &SkSurface::flush)
         .function("makeImageSnapshot", &SkSurface::makeImageSnapshot)
         .function("_readPixels", optional_override([](SkSurface& self, int width, int height, uintptr_t /* uint8_t* */ cptr)->bool {
             auto* dst = reinterpret_cast<uint8_t*>(cptr);
@@ -358,5 +362,6 @@
         }), allow_raw_pointers());
 
     function("MakeAnimation", &MakeAnimation);
+    constant("skottie", true);
 #endif
 }
diff --git a/experimental/canvaskit/compile.sh b/experimental/canvaskit/compile.sh
index 67be659..7cfdd04 100755
--- a/experimental/canvaskit/compile.sh
+++ b/experimental/canvaskit/compile.sh
@@ -13,8 +13,6 @@
   exit 1
 fi
 
-BUILD_DIR=${BUILD_DIR:="out/canvaskit_wasm"}
-mkdir -p $BUILD_DIR
 # Navigate to SKIA_HOME from where this file is located.
 pushd $BASE_DIR/../..
 
@@ -24,17 +22,24 @@
 
 RELEASE_CONF="-Oz --closure 1 --llvm-lto 3 -DSK_RELEASE"
 EXTRA_CFLAGS="\"-DSK_RELEASE\""
+
 if [[ $@ == *debug* ]]; then
   echo "Building a Debug build"
   EXTRA_CFLAGS="\"-DSK_DEBUG\""
-  RELEASE_CONF="-O0 --js-opts 0 -s SAFE_HEAP=1 -s ASSERTIONS=1 -s GL_ASSERTIONS=1 -g3 -DPATHKIT_TESTING -DSK_DEBUG"
+  RELEASE_CONF="-O0 --js-opts 0 -s DEMANGLE_SUPPORT=1 -s SAFE_HEAP=1 -s ASSERTIONS=1 -s GL_ASSERTIONS=1 -g3 -DPATHKIT_TESTING -DSK_DEBUG"
+  BUILD_DIR=${BUILD_DIR:="out/canvaskit_wasm_debug"}
+else
+  BUILD_DIR=${BUILD_DIR:="out/canvaskit_wasm"}
 fi
 
+mkdir -p $BUILD_DIR
+
 GN_GPU="skia_enable_gpu=true"
 WASM_GPU="-lEGL -lGLESv2 -DSK_SUPPORT_GPU=1"
-if [[ $@ == *no_gpu* ]]; then
-  echo "Omitting the GPU backend"
+if [[ $@ == *cpu* ]]; then
+  echo "Using the CPU backend instead of the GPU backend"
   GN_GPU="skia_enable_gpu=false"
+  GN_GPU_FLAGS=""
   WASM_GPU="-DSK_SUPPORT_GPU=0"
 fi
 
diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js
index c0f6c66..801bad6 100644
--- a/experimental/canvaskit/externs.js
+++ b/experimental/canvaskit/externs.js
@@ -31,6 +31,8 @@
 	MakeSkDashPathEffect: function(intervals, phase) {},
 	setCurrentContext: function() {},
 	LTRBRect: function(l, t, r, b) {},
+	gpu: {},
+	skottie: {},
 
 	// private API (i.e. things declared in the bindings that we use
 	// in the pre-js file)
@@ -78,9 +80,11 @@
 	SkSurface: {
 		// public API should go below because closure still will
 		// remove things declared here and not on the prototype.
+		flush: function() {},
 
 		// private API
 		_readPixels: function(w, h, ptr) {},
+		_flush: function() {},
 	}
 }
 
diff --git a/experimental/canvaskit/interface.js b/experimental/canvaskit/interface.js
index 98be5ef..938ef3e 100644
--- a/experimental/canvaskit/interface.js
+++ b/experimental/canvaskit/interface.js
@@ -107,49 +107,55 @@
       return this;
     };
 
-    CanvasKit.SkSurface.prototype.flush = function() {
-      var success = this._readPixels(this._width, this._height, this._pixelPtr);
-      if (!success) {
-        console.err('could not read pixels');
-        return;
+    if (CanvasKit.gpu) {
+      CanvasKit.getWebGLSurface = function(htmlID) {
+        var canvas = document.getElementById(htmlID);
+        if (!canvas) {
+          throw 'Canvas with id ' + htmlID + ' was not found';
+        }
+        // Maybe better to use clientWidth/height.  See:
+        // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
+        return this._getWebGLSurface(htmlID, canvas.width, canvas.height);
+      };
+
+      CanvasKit.SkSurface.prototype.flush = function() {
+        this._flush();
       }
+    } else {
+      CanvasKit.getRasterN32PremulSurface = function(htmlID) {
+        var canvas = document.getElementById(htmlID);
+        if (!canvas) {
+          throw 'Canvas with id ' + htmlID + ' was not found';
+        }
+        // Maybe better to use clientWidth/height.  See:
+        // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
+        var surface = this._getRasterN32PremulSurface(canvas.width, canvas.height);
+        if (surface) {
+          surface.canvas = canvas;
+          surface._width = canvas.width;
+          surface._height = canvas.height;
+          surface._pixelLen = surface._width * surface._height * 4; // it's 8888
+          // Allocate the buffer of pixels to be used to draw back and forth.
+          surface._pixelPtr = CanvasKit._malloc(surface._pixelLen);
+        }
+        return surface;
+      };
 
-      var pixels = new Uint8ClampedArray(CanvasKit.buffer, this._pixelPtr, this._pixelLen);
-      var imageData = new ImageData(pixels, this._width, this._height);
+      CanvasKit.SkSurface.prototype.flush = function() {
+        this._flush();
+        var success = this._readPixels(this._width, this._height, this._pixelPtr);
+        if (!success) {
+          console.err('could not read pixels');
+          return;
+        }
 
-      this.canvas.getContext('2d').putImageData(imageData, 0, 0);
+        var pixels = new Uint8ClampedArray(CanvasKit.buffer, this._pixelPtr, this._pixelLen);
+        var imageData = new ImageData(pixels, this._width, this._height);
 
-    };
-  }
-
-  CanvasKit.getWebGLSurface = function(htmlID) {
-    var canvas = document.getElementById(htmlID);
-    if (!canvas) {
-      throw 'Canvas with id ' + htmlID + ' was not found';
+        this.canvas.getContext('2d').putImageData(imageData, 0, 0);
+      };
     }
-    // Maybe better to use clientWidth/height.  See:
-    // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
-    return this._getWebGLSurface(htmlID, canvas.width, canvas.height);
-  }
-
-  CanvasKit.getRasterN32PremulSurface = function(htmlID) {
-    var canvas = document.getElementById(htmlID);
-    if (!canvas) {
-      throw 'Canvas with id ' + htmlID + ' was not found';
-    }
-    // Maybe better to use clientWidth/height.  See:
-    // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
-    var surface = this._getRasterN32PremulSurface(canvas.width, canvas.height);
-    if (surface) {
-      surface.canvas = canvas;
-      surface._width = canvas.width;
-      surface._height = canvas.height;
-      surface._pixelLen = surface._width * surface._height * 4; // it's 8888
-      // Allocate the buffer of pixels to be used to draw back and forth.
-      surface._pixelPtr = CanvasKit._malloc(surface._pixelLen);
-    }
-    return surface;
-  }
+  } // end CanvasKit.onRuntimeInitialized, that is, anything changing prototypes or dynamic.
 
   // Likely only used for tests.
   CanvasKit.LTRBRect = function(l, t, r, b) {
diff --git a/experimental/canvaskit/karma.conf.js b/experimental/canvaskit/karma.conf.js
new file mode 100644
index 0000000..31f5b46
--- /dev/null
+++ b/experimental/canvaskit/karma.conf.js
@@ -0,0 +1,72 @@
+const isDocker = require('is-docker')();
+
+module.exports = function(config) {
+  // Set the default values to be what are needed when testing the
+  // WebAssembly build locally.
+  let cfg = {
+    // frameworks to use
+    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+    frameworks: ['jasmine'],
+
+    // list of files / patterns to load in the browser
+    files: [
+      { pattern: 'canvaskit/bin/canvaskit.wasm', included:false, served:true},
+      '../../modules/pathkit/tests/testReporter.js',
+      'canvaskit/bin/canvaskit.js',
+      'tests/*.spec.js'
+    ],
+
+    proxies: {
+      '/canvaskit/': '/base/canvaskit/bin/'
+    },
+
+    // test results reporter to use
+    // possible values: 'dots', 'progress'
+    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+    reporters: ['progress'],
+
+    // web server port
+    port: 4444,
+
+    // enable / disable colors in the output (reporters and logs)
+    colors: true,
+
+    // level of logging
+    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: true,
+
+    browserDisconnectTimeout: 15000,
+    browserNoActivityTimeout: 15000,
+
+    // start these browsers
+    browsers: ['Chrome'],
+
+    // Continuous Integration mode
+    // if true, Karma captures browsers, runs the tests and exits
+    singleRun: false,
+
+    // Concurrency level
+    // how many browser should be started simultaneous
+    concurrency: Infinity,
+  };
+
+  if (isDocker) {
+    // See https://hackernoon.com/running-karma-tests-with-headless-chrome-inside-docker-ae4aceb06ed3
+    cfg.browsers = ['ChromeHeadlessNoSandbox'],
+    cfg.customLaunchers = {
+        ChromeHeadlessNoSandbox: {
+            base: 'ChromeHeadless',
+            flags: [
+            // Without this flag, we see an error:
+            // Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted
+                '--no-sandbox'
+            ],
+        },
+    };
+  }
+
+  config.set(cfg);
+}
diff --git a/experimental/canvaskit/package.json b/experimental/canvaskit/package.json
index 9986143..3a0a862 100644
--- a/experimental/canvaskit/package.json
+++ b/experimental/canvaskit/package.json
@@ -4,15 +4,14 @@
   "description": "private",
   "private": true,
   "main": "index.js",
-  "dependencies": {
-  },
+  "dependencies": {},
   "devDependencies": {
-    "is-docker": "^1.1.0",
-    "jasmine-core": "^3.1.0",
-    "karma": "^2.0.5",
-    "karma-chrome-launcher": "^2.2.0",
-    "karma-jasmine": "^1.1.2",
-    "requirejs": "^2.3.5"
+    "is-docker": "~1.1.0",
+    "jasmine-core": "~3.1.0",
+    "karma": "~3.0.0",
+    "karma-chrome-launcher": "~2.2.0",
+    "karma-jasmine": "~1.1.2",
+    "requirejs": "~2.3.5"
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
diff --git a/experimental/canvaskit/tests/path.spec.js b/experimental/canvaskit/tests/path.spec.js
new file mode 100644
index 0000000..0be3032
--- /dev/null
+++ b/experimental/canvaskit/tests/path.spec.js
@@ -0,0 +1,166 @@
+
+describe('CanvasKit\'s Path Behavior', function() {
+    // Note, don't try to print the CanvasKit object - it can cause Karma/Jasmine to lock up.
+    var CanvasKit = null;
+    const LoadCanvasKit = new Promise(function(resolve, reject) {
+        if (CanvasKit) {
+            resolve();
+        } else {
+            CanvasKitInit({
+                locateFile: (file) => '/canvaskit/'+file,
+            }).then((_CanvasKit) => {
+                CanvasKit = _CanvasKit;
+                CanvasKit.initFonts();
+                resolve();
+            });
+        }
+    });
+
+    let container = document.createElement('div');
+    document.body.appendChild(container);
+    const CANVAS_WIDTH = 600;
+    const CANVAS_HEIGHT = 600;
+
+    beforeEach(function() {
+        container.innerHTML = `
+            <canvas width=600 height=600 id=test></canvas>
+            <canvas width=600 height=600 id=report></canvas>`;
+    });
+
+    afterEach(function() {
+        container.innerHTML = '';
+    });
+
+    function getSurface() {
+        if (CanvasKit.gpu) {
+            return CanvasKit.getWebGLSurface('test');
+        }
+        return CanvasKit.getRasterN32PremulSurface('test');
+    }
+
+
+    function reportSurface(surface, testname, done) {
+        // In docker, the webgl canvas is blank, but the surface has the pixel
+        // data. So, we copy it out and draw it to a normal canvas to take a picture.
+        // To be consistent across CPU and GPU, we just do it for all configurations
+        // (even though the CPU canvas shows up after flush just fine).
+        let pixelLen = CANVAS_WIDTH * CANVAS_HEIGHT * 4; // 4 bytes for r,g,b,a
+        let pixelPtr = CanvasKit._malloc(pixelLen);
+        let success = surface._readPixels(CANVAS_WIDTH, CANVAS_HEIGHT, pixelPtr);
+        if (!success) {
+            done();
+            expect(success).toBeFalsy('could not read pixels');
+            return;
+        }
+        let pixels = new Uint8ClampedArray(CanvasKit.buffer, pixelPtr, pixelLen);
+        var imageData = new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT);
+
+        let reportingCanvas =  document.getElementById('report');
+        reportingCanvas.getContext('2d').putImageData(imageData, 0, 0);
+        CanvasKit._free(pixelPtr);
+        reportCanvas(reportingCanvas, testname).then(() => {
+            done();
+        }).catch(reportError(done));
+    }
+
+    it('can draw a path', function(done) {
+        LoadCanvasKit.then(() => {
+            // This is taken from example.html
+            const surface = getSurface();
+            expect(surface).toBeTruthy('Could not make surface')
+            if (!surface) {
+                done();
+                return;
+            }
+            const canvas = surface.getCanvas();
+            const paint = new CanvasKit.SkPaint();
+            paint.setStrokeWidth(1.0);
+            paint.setAntiAlias(true);
+            paint.setColor(CanvasKit.Color(0, 0, 0, 1.0));
+            paint.setStyle(CanvasKit.PaintStyle.STROKE);
+
+            const path = new CanvasKit.SkPath();
+            path.moveTo(20, 5);
+            path.lineTo(30, 20);
+            path.lineTo(40, 10);
+            path.lineTo(50, 20);
+            path.lineTo(60, 0);
+            path.lineTo(20, 5);
+
+            path.moveTo(20, 80);
+            path.cubicTo(90, 10, 160, 150, 190, 10);
+
+            path.moveTo(36, 148);
+            path.quadTo(66, 188, 120, 136);
+            path.lineTo(36, 148);
+
+            path.moveTo(150, 180);
+            path.arcTo(150, 100, 50, 200, 20);
+            path.lineTo(160, 160);
+
+            path.moveTo(20, 120);
+            path.lineTo(20, 120);
+
+            path.transform([2, 0, 0,
+                            0, 2, 0,
+                            0, 0, 1 ])
+
+            canvas.drawPath(path, paint);
+            surface.flush();
+
+            path.delete();
+            paint.delete();
+
+            reportSurface(surface, 'path_api_example', done);
+        });
+        // See CanvasKit for more tests, since they share implementation
+    });
+
+    function starPath(CanvasKit, X=128, Y=128, R=116) {
+        let p = new CanvasKit.SkPath();
+        p.moveTo(X + R, Y);
+        for (let i = 1; i < 8; i++) {
+          let a = 2.6927937 * i;
+          p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
+        }
+        return p;
+      }
+
+    it('can apply an effect and draw text', function(done) {
+        LoadCanvasKit.then(() => {
+            const surface = getSurface();
+            expect(surface).toBeTruthy('Could not make surface')
+            if (!surface) {
+                done();
+                return;
+            }
+            const canvas = surface.getCanvas();
+            const path = starPath(CanvasKit);
+
+            const paint = new CanvasKit.SkPaint();
+
+            const textPaint = new CanvasKit.SkPaint();
+            textPaint.setColor(CanvasKit.Color(40, 0, 0, 1.0));
+            textPaint.setTextSize(30);
+            textPaint.setAntiAlias(true);
+
+            const dpe = CanvasKit.MakeSkDashPathEffect([15, 5, 5, 10], 1);
+
+            paint.setPathEffect(dpe);
+            paint.setStyle(CanvasKit.PaintStyle.STROKE);
+            paint.setStrokeWidth(5.0);
+            paint.setAntiAlias(true);
+            paint.setColor(CanvasKit.Color(66, 129, 164, 1.0));
+
+            canvas.clear(CanvasKit.Color(255, 255, 255, 1.0));
+
+            canvas.drawPath(path, paint);
+            canvas.drawText('This is text', 10, 280, textPaint);
+            surface.flush();
+            dpe.delete();
+            path.delete();
+
+            reportSurface(surface, 'effect_and_text_example', done);
+        });
+    });
+});