| # Okay so maybe everyone else already knows all this, but it took some time |
| # for Michael and I [Han] to really see how everything fits together. |
| # |
| # Basically, what we're doing here is automated browser testing, so CircleCI |
| # handles the automation, and Sauce Labs handles the browser testing. |
| # Specifically, Sauce Labs offers a REST API to run tests in browsers in VMs, |
| # and CircleCI can be configured to listen for git pushes and run local |
| # servers and call out to REST APIs to test against these local servers. |
| # |
| # The flow goes like this: |
| # - CircleCI notices/is notified of a git push |
| # - they pull and checkout and magically know to install dependencies and shit |
| # + https://circleci.com/docs/manually/ |
| # - their magic works fine for MathQuill's dependencies but to run the tests, |
| # it foolishly runs `make test`, what an inconceivable mistake |
| # - that's where we come in: `circle.yml` lets us override the test script. |
| # + https://circleci.com/docs/configuration/ |
| # - our `circle.yml` first installs and runs a tunnel to Sauce Labs |
| # - and runs `make server` |
| # - then it calls out to Sauce Labs' REST API to open browsers that reach |
| # back through the tunnel to access test pages on the local server |
| # + > Sauce Connect allows you to run a test server within the CircleCI |
| # > build container and expose it it (using a URL like `localhost:8080`) |
| # > to Sauce Labs’ browsers. |
| # |
| # https://circleci.com/docs/browser-testing-with-sauce-labs/ |
| # |
| # - boom testing boom |
| |
| |
| # this file is based on https://github.com/circleci/sauce-connect/blob/a65e41c91e02550ce56c75740a422bebc4acbf6f/circle.yml |
| # via https://circleci.com/docs/browser-testing-with-sauce-labs/ |
| |
| dependencies: |
| cache_directories: |
| - ~/sauce-connect |
| pre: |
| - ? |- |
| # SauceConnect: download if not cached, and launch with retry |
| test $SAUCE_USERNAME && test $SAUCE_ACCESS_KEY || { |
| echo 'Sauce Labs credentials required. Sign up here: https://saucelabs.com/opensauce/' |
| exit 1 |
| } |
| |
| mkdir -p ~/sauce-connect |
| cd ~/sauce-connect |
| |
| if [ -x sc-*-linux/bin/sc ]; then |
| echo Using cached sc-*-linux/bin/sc |
| else |
| time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz |
| time tar -xzf sc-latest-linux.tar.gz |
| fi |
| |
| time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ |
| --readyfile ~/sauce_is_ready |
| test -e ~/sauce_was_ready && exit |
| |
| echo 'Sauce Connect failed, try redownloading (https://git.io/vSxsJ)' |
| rm -rf * |
| time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz |
| time tar -xzf sc-latest-linux.tar.gz |
| |
| time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ |
| --readyfile ~/sauce_is_ready |
| test -e ~/sauce_was_ready && exit |
| |
| echo 'ERROR: Exited twice without creating readyfile' \ |
| | tee /dev/stderr > ~/sauce_is_ready |
| exit 1 |
| : |
| background: true |
| |
| test: |
| pre: |
| - |- |
| # Generate link to Many-Worlds build and add to GitHub Commit Status |
| curl -i -X POST https://api.github.com/repos/mathquill/mathquill/statuses/$CIRCLE_SHA1 \ |
| -u MathQuillBot:$GITHUB_STATUS_API_KEY \ |
| -d '{ |
| "context": "ci/many-worlds", |
| "state": "success", |
| "description": "Try the tests on the Many-Worlds build of this commit:", |
| "target_url": "http://many-worlds.glitch.me/mathquill/mathquill/commit/'$CIRCLE_SHA1'/test/" |
| }' |
| |
| # Safari on Sauce can only connect to port 3000, 4000, 7000, or 8000. Edge needs port 7000 or 8000. |
| # https://david263a.wordpress.com/2015/04/18/fixing-safari-cant-connect-to-localhost-issue-when-using-sauce-labs-connect-tunnel/ |
| # https://support.saucelabs.com/customer/portal/questions/14368823-requests-to-localhost-on-microsoft-edge-are-failing-over-sauce-connect |
| - PORT=8000 make server: |
| background: true |
| |
| # Wait for tunnel to be ready (`make server` is much faster, no need to wait for it) |
| - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done; touch ~/sauce_was_ready; test -z "$(<~/sauce_is_ready)" |
| |
| override: |
| - ? |- |
| # Screenshots: capture in the background while running unit tests |
| mkdir -p $CIRCLE_TEST_REPORTS/mocha |
| |
| # CircleCI expects test results to be reported in an JUnit/xUnit-style XML file: |
| # https://circleci.com/docs/test-metadata/#a-namemochajsamocha-for-nodejs |
| # Our unit tests are in a browser, so they can't write to a file, and Sauce |
| # apparently truncates custom data in their test result reports, so instead we |
| # POST to this trivial Node server on localhost:9000 that writes the body of |
| # any POST request to $CIRCLE_TEST_REPORTS/junit/test-results.xml |
| node -e ' |
| require("http").createServer(function(req, res) { |
| res.setHeader("Access-Control-Allow-Origin", "*"); |
| req.pipe(process.stdout); |
| req.on("end", res.end.bind(res)); |
| }) |
| .listen(9000); |
| console.error("listening on http://0.0.0.0:9000/"); |
| ' 2>&1 >$CIRCLE_TEST_REPORTS/junit/test-results.xml | { |
| # ^ note: `2>&1` must precede `>$CIRCLE_TEST_REPORTS/...` because |
| # shell redirect is like assignment; if it came after, then both |
| # stdout and stderr would be written to `xunit.xml` and nothing |
| # would be piped into here |
| |
| head -1 # wait for "listening on ..." to be logged |
| |
| # https://circleci.com/docs/environment-variables/ |
| build_name="CircleCI build #$CIRCLE_BUILD_NUM" |
| if [ $CIRCLE_PR_NUMBER ]; then |
| build_name="$build_name: PR #$CIRCLE_PR_NUMBER" |
| [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)" |
| else |
| build_name="$build_name: $CIRCLE_BRANCH" |
| fi |
| build_name="$build_name @ ${CIRCLE_SHA1:0:7}" |
| export MQ_CI_BUILD_NAME="$build_name" |
| |
| time { test -d node_modules/wd || npm install wd; } |
| time node script/screenshots.js http://localhost:8000/test/visual.html \ |
| && touch ~/screenshots_are_ready || echo EXIT STATUS $? | tee /dev/stderr > ~/screenshots_are_ready: |
| } |
| : |
| background: true |
| |
| - |- |
| # Unit tests in the browser |
| |
| echo '1. Launch tests' |
| echo |
| |
| # https://circleci.com/docs/environment-variables/ |
| build_name="CircleCI build #$CIRCLE_BUILD_NUM" |
| if [ $CIRCLE_PR_NUMBER ]; then |
| build_name="$build_name: PR #$CIRCLE_PR_NUMBER" |
| [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)" |
| else |
| build_name="$build_name: $CIRCLE_BRANCH" |
| fi |
| build_name="$build_name @ ${CIRCLE_SHA1:0:7}" |
| |
| # "build" and "customData" parameters from: |
| # https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-TestAnnotation |
| set -o pipefail |
| curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests \ |
| -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \ |
| -H 'Content-Type: application/json' \ |
| -d '{ |
| "name": "Unit tests, Mocha", |
| "build": "'"$build_name"'", |
| "customData": {"build_url": "'"$CIRCLE_BUILD_URL"'"}, |
| "framework": "mocha", |
| "url": "http://localhost:8000/test/unit.html?post_xunit_to=http://localhost:9000", |
| "platforms": [["", "Chrome", ""]] |
| }' \ |
| | tee /dev/stderr | tail -1 > js-tests.json |
| |
| echo '2. Wait for tests to finish:' |
| echo |
| # > Make the request multiple times as the tests run until the response |
| # > contains `completed: true` to the get the final results. |
| # https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods |
| while true # Bash has no do...while >:( |
| do |
| sleep 5 |
| curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests/status \ |
| -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \ |
| -H 'Content-Type: application/json' \ |
| -d @js-tests.json \ |
| | tee /dev/stderr | tail -1 > status.json |
| |
| # deliberately do `... != false` rather than `... == true` |
| # because unexpected values should break rather than infinite loop |
| [ "$(jq .completed <status.json)" != false ] && break |
| done |
| |
| echo '3. Exit with non-zero status code if any unit tests failed' |
| exit "$(jq '.["js tests"][0].result.failures' <status.json)" |
| |
| - |- |
| # Stitch together screenshots and diff against master |
| |
| echo '0. Wait for screenshots to be ready' |
| while [ ! -e ~/screenshots_are_ready ]; do sleep 1; done |
| test -z "$(<~/screenshots_are_ready)" || exit 1 |
| |
| echo '1. Stitch together pieces' |
| for img in $(ls $CIRCLE_ARTIFACTS/imgs/pieces/); do |
| convert $(ls -1 $CIRCLE_ARTIFACTS/imgs/pieces/$img/*.png | sort -n) -append $CIRCLE_ARTIFACTS/imgs/$img.png |
| done |
| |
| echo '2. Download the latest screenshots from master' |
| echo |
| |
| artifacts_json="$(curl https://circleci.com/api/v1/project/mathquill/mathquill/latest/artifacts?branch=master)" |
| echo |
| echo '/latest/artifacts?branch=master:' |
| echo |
| echo "$artifacts_json" |
| echo |
| |
| mkdir $CIRCLE_ARTIFACTS/imgs/baseline/ |
| baseline_imgs="$(echo "$artifacts_json" \ |
| | jq -r '.[] | .url + " -o " + .pretty_path' \ |
| | grep '\.png$' \ |
| | grep -v '_DIFF\.png$' \ |
| | grep -vF '/pieces/' \ |
| | grep -vF '/baseline/' \ |
| | sed "s:\$CIRCLE_ARTIFACTS/imgs/:$CIRCLE_ARTIFACTS/imgs/baseline/:")" |
| echo 'Baseline image URLs and files:' |
| echo |
| echo "$baseline_imgs" |
| echo |
| |
| test -z "$baseline_imgs" && { echo 'No baseline images to download'; exit; } |
| curl $baseline_imgs |
| echo |
| |
| echo '3. Generate image diffs' |
| echo |
| cd $CIRCLE_ARTIFACTS/imgs/ |
| for file in $(ls *.png); do |
| # if evergreen browser, browser version of previous screenshot may not match, |
| # so replace previous browser version with glob |
| baseline="$(echo baseline/$(echo $file | sed 's/[^_]*_(evergreen)/*/; s/OS_X_.*/OS_X_*.png/' | tee /dev/stderr) | tee /dev/stderr)" |
| echo "Number of different pixels from baseline in $file:" |
| compare -metric AE $baseline $file ${file/%.png/_DIFF.png} |
| echo |
| done |
| true # ignore errors like "image widths or heights differ" |
| |
| post: |
| - killall --wait sc; true # wait for Sauce Connect to close the tunnel; ignore errors since it's just cleanup |