├── img └── logo.png ├── src ├── index.js ├── browser.js └── piri-piri.js ├── tests ├── scripts │ ├── simple.js │ └── method.js └── test-piri-piri.js ├── docker-experiment ├── Dockerfile.txt ├── Dockerfile.tar └── whaleBrowsing.js ├── .gitignore ├── package.json ├── LICENSE └── README.md /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daviddias/piri-piri/HEAD/img/logo.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | exports = module.exports = require('./piri-piri.js') 2 | exports.client = require('./browser.js') 3 | -------------------------------------------------------------------------------- /tests/scripts/simple.js: -------------------------------------------------------------------------------- 1 | const ppc = require('../../src').client 2 | 3 | module.exports = function (args) { 4 | ppc.connect((err, socket) => { 5 | if (err) { 6 | return console.log(err) 7 | } 8 | socket.on('exit', ppc.exit) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /tests/scripts/method.js: -------------------------------------------------------------------------------- 1 | const ppc = require('../../src').client 2 | 3 | module.exports = function (args) { 4 | ppc.handle('sum', (arr) => { 5 | var sum = Number(arr[0] + arr[1]) 6 | ppc.send(sum) 7 | }) 8 | 9 | ppc.connect((err) => { 10 | if (err) { 11 | return console.log(err) 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /docker-experiment/Dockerfile.txt: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN \ 4 | apt-get update && \ 5 | apt-get install -y wget xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-mathml && \ 6 | wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ 7 | echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list && \ 8 | apt-get update && \ 9 | apt-get install -y google-chrome-stable xvfb 10 | 11 | CMD ["bash"] 12 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | const SocketIO = require('socket.io-client') 2 | 3 | var socket 4 | const handles = {} 5 | 6 | exports = module.exports 7 | 8 | exports.connect = (callback) => { 9 | socket = SocketIO.connect('http://localhost:9046') 10 | socket.on('exit', exports.exit) 11 | Object.keys(handles).forEach((action) => { 12 | socket.on(action, handles[action]) 13 | }) 14 | callback(null, socket) 15 | } 16 | 17 | exports.exit = () => { 18 | require('remote').require('app').quit() 19 | } 20 | 21 | exports.handle = (action, func) => { 22 | handles[action] = func 23 | } 24 | 25 | exports.send = (msg) => { 26 | socket.emit('msg', msg) 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piri-piri", 3 | "version": "0.4.0", 4 | "description": "piri-piri is a browser orchestration to enable decentralized browser applications tests. Ah and it is hot :)", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha tests/test-*.js", 8 | "lint": "standard", 9 | "test-old": "node ./node_modules/.bin/lab tests/spicy-test.js", 10 | "test-farm": "node ./node_modules/.bin/lab tests/farm-test.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/diasdavid/piri-piri.git" 15 | }, 16 | "author": "David Dias ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/diasdavid/piri-piri/issues" 20 | }, 21 | "homepage": "https://github.com/diasdavid/piri-piri", 22 | "dependencies": { 23 | "electron-spawn": "^3.3.0", 24 | "socket.io": "^1.4.5", 25 | "socket.io-client": "^1.4.5" 26 | }, 27 | "devDependencies": { 28 | "chai": "^3.5.0", 29 | "mocha": "^2.4.5", 30 | "piri-piri.client": "^0.3.0", 31 | "pre-commit": "^1.1.2", 32 | "standard": "^6.0.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 David Dias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tests/test-piri-piri.js: -------------------------------------------------------------------------------- 1 | /* globals describe, it */ 2 | 3 | const expect = require('chai').expect 4 | const pp = require('../src') 5 | 6 | describe('piri-piri', () => { 7 | it('start piri-piri', (done) => { 8 | pp.start((err) => { 9 | expect(err).to.not.exist 10 | done() 11 | }) 12 | }) 13 | 14 | it('connect one and exit', (done) => { 15 | expect(Object.keys(pp.clients).length).to.equal(0) 16 | pp.browser.spawn('./tests/scripts/simple.js', 1, (err) => { 17 | expect(err).to.not.exist 18 | done() 19 | }) 20 | setTimeout(() => { 21 | var id = Object.keys(pp.clients)[0] 22 | pp.browser.send(id, 'exit') 23 | }, 800) 24 | }) 25 | 26 | it('connect two and exit', (done) => { 27 | expect(Object.keys(pp.clients).length).to.equal(0) 28 | pp.browser.spawn('./tests/scripts/simple.js', 2, (err) => { 29 | expect(err).to.not.exist 30 | done() 31 | }) 32 | setTimeout(() => { 33 | Object.keys(pp.clients).forEach((id) => { 34 | pp.browser.send(id, 'exit') 35 | }) 36 | }, 800) 37 | }) 38 | 39 | it('connect one, call a method and exit', (done) => { 40 | expect(Object.keys(pp.clients).length).to.equal(0) 41 | pp.browser.spawn('./tests/scripts/method.js', 1, (err) => { 42 | expect(err).to.not.exist 43 | done() 44 | }) 45 | setTimeout(() => { 46 | var id = Object.keys(pp.clients)[0] // should be 0, electron is not being properly closed 47 | pp.browser.send(id, 'sum', 2, 2) 48 | setTimeout(() => { 49 | expect(pp.clients[id].msgs.length).to.equal(1) 50 | expect(pp.clients[id].msgs[0]).to.equal(4) 51 | pp.browser.send(id, 'exit') 52 | }, 1000) 53 | }, 800) 54 | }) 55 | 56 | it.skip('connect two, call a p2p method chain and exit', (done) => {}) 57 | }) 58 | -------------------------------------------------------------------------------- /docker-experiment/Dockerfile.tar: -------------------------------------------------------------------------------- 1 | Dockerfile000644 000765 000024 00000000645 12424324470 013470 0ustar00apocasstaff000000 000000 FROM ubuntu 2 | 3 | RUN \ 4 | apt-get update && \ 5 | apt-get install -y wget xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-mathml && \ 6 | wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ 7 | echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list && \ 8 | apt-get update && \ 9 | apt-get install -y google-chrome-stable xvfb 10 | 11 | CMD ["bash"] 12 | -------------------------------------------------------------------------------- /docker-experiment/whaleBrowsing.js: -------------------------------------------------------------------------------- 1 | var Docker = require('dockerode') 2 | var fs = require('fs') 3 | // var docker = new Docker({ 4 | // socketPath: '/var/run/docker.sock' 5 | // }) 6 | 7 | // https://github.com/apocas/dockerode/issues/100 8 | 9 | var docker = new Docker({ 10 | protocol: 'https', 11 | host: '192.168.59.103', 12 | port: process.env.DOCKER_PORT || 2376, 13 | ca: fs.readFileSync(process.env.DOCKER_CERT_PATH + '/ca.pem'), 14 | cert: fs.readFileSync(process.env.DOCKER_CERT_PATH + '/cert.pem'), 15 | key: fs.readFileSync(process.env.DOCKER_CERT_PATH + '/key.pem') 16 | }) 17 | 18 | // var docker = new Docker({protocol:'tcp', host: '192.168.59.103', port: 2376}) 19 | // xvfb-run --server-args='-screen 0, 1024x768x16' google-chrome -start-maximized http://7631f333.ngrok.com > /dev/null & 20 | 21 | // xvfb-run -e /dev/stdout --server-args='-screen 0, 1024x768x16' google-chrome --disable-webgl -start-maximized http://7631f333.ngrok.com & 22 | 23 | // test the funky business of GPU 24 | // git clone 25 | // vagrant up 26 | // vagrant ssh 27 | // cd /vagrant 28 | // npm i 29 | // cd /examples/build 30 | // node run.js 31 | 32 | docker.buildImage('./Dockerfile.tar', {t: 'chrome'}, function (err, stream) { 33 | if (err) { 34 | return console.log(err) 35 | } 36 | 37 | stream.pipe(process.stdout, {end: true}) 38 | 39 | stream.on('end', function () { 40 | console.log('image mounted') 41 | // done() 42 | }) 43 | }) 44 | 45 | // function done() { 46 | // docker.createContainer({ 47 | // Image: 'chrome', 48 | // Cmd: ['/bin/bash', '-c', 'xvfb-run -e /dev/stdout --server-args=\'-screen 0, 1024x768x16\' google-chrome -start-maximized http://7631f333.ngrok.com'] 49 | // }, function(err, container) { 50 | // container.attach({ 51 | // stream: true, 52 | // stdout: true, 53 | // stderr: true, 54 | // tty: true 55 | // }, function(err, stream) { 56 | // if(err) return 57 | 58 | // stream.pipe(process.stdout) 59 | 60 | // container.start({ 61 | // Privileged: true 62 | // }, function(err, data) { 63 | // if(err) return 64 | // }) 65 | // }) 66 | // }) 67 | // } 68 | -------------------------------------------------------------------------------- /src/piri-piri.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron-spawn') 2 | const SocketIO = require('socket.io') 3 | const http = require('http') 4 | const debug = require('debug') 5 | const log = debug('piri-piri') 6 | 7 | var started = false 8 | const clients = {} // socket and msgs 9 | 10 | exports = module.exports 11 | 12 | exports.clients = clients 13 | 14 | exports.start = (callback) => { 15 | const server = http.createServer((req, res) => {}) 16 | server.listen(9046) 17 | const io = new SocketIO(server) 18 | started = true 19 | 20 | io.on('connection', (sock) => { 21 | log('new conn', sock.id) 22 | 23 | clients[sock.id] = {} 24 | clients[sock.id].socket = sock 25 | clients[sock.id].msgs = [] 26 | 27 | sock.on('msg', (data) => { 28 | clients[sock.id].msgs.push(data) 29 | }) 30 | 31 | sock.on('close', () => { 32 | delete clients[sock.id] 33 | }) 34 | }) 35 | callback() 36 | } 37 | 38 | exports.browser = {} 39 | 40 | exports.browser.spawn = (scriptPath, quantity, callback) => { 41 | if (!started) { 42 | throw new Error('piri-piri listener is not started yet') 43 | } 44 | 45 | var counter = 0 46 | while (counter < quantity) { 47 | spawnOne(scriptPath) 48 | counter += 1 49 | } 50 | 51 | function spawnOne (scriptPath) { 52 | const instance = electron(scriptPath, { 53 | detached: true 54 | }) 55 | 56 | const errors = [] 57 | 58 | instance.stderr.on('data', function (data) { 59 | errors.push(data.toString()) 60 | }) 61 | 62 | instance.stdout.on('data', function (data) { 63 | console.log(data.toString()) 64 | }) 65 | 66 | instance.on('exit', () => { 67 | counter-- 68 | if (counter === 0) { 69 | end() 70 | } 71 | }) 72 | 73 | function end () { 74 | if (errors.length > 0) { 75 | return callback(errors) 76 | } 77 | callback() 78 | } 79 | } 80 | } 81 | 82 | exports.browser.send = function (id, action) { 83 | if (!clients[id]) { 84 | throw new Error('no client with that Id') 85 | } 86 | 87 | var args = Object.keys(arguments).map((key) => { return arguments[key] }) 88 | 89 | args.shift() 90 | args.shift() 91 | 92 | clients[id].socket.emit(action, args) 93 | 94 | if (action === 'exit') { 95 | delete clients[id] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | piri-piri 2 | ========= 3 | 4 | > [**`piri-piri`**](https://en.wikipedia.org/wiki/Piri_piri) is a browser orchestration to enable decentralized browser applications tests. Ah and it is hot :) 5 | 6 | [![](https://img.shields.io/badge/project-WebCompute-blue.svg?style=flat-square)](https://github.com/diasdavid/WebCompute) 7 | 8 | ![](/img/logo.png) 9 | 10 | 11 | # Why does it exist 12 | 13 | There are a panoply of excellent browser testing frameworks and services available today, however their focus is on testing browser implementations (CSS, HTML and JavaScript) and user interactions of the apps their are testing (clicks, mouse movements, what the user sees). 14 | 15 | When it comes to testing to test a decentralized browser app or library, the focus stops being how a browser implements a specific behaviour, but how the decentralized network handles node joins and leaves and if nodes are effectively communicating between each other. In this scenario, we have several events that the server never sees or that the server never instructs the clients to do, so we need to create a new way to coordinate the browser joins and leaves and also how they interact between each other remotely and this is were `piri-piri` comes into play. 16 | 17 | 18 | The specific set of problems `piri-piri` tries to solve: 19 | 20 | - browser times X, where 1<=X<=virtually unlimited - Most browser testing frameworks only let you launch a couple of browsers at a time, `piri-piri` aims to launch several browsers and/or tabs to load a webpage, in a local or distributed fashion. 21 | 22 | - instruct browsers on demand - Since there is a ton of stuff happening on browser decentralized apps, we can't just write a script to test and listen to events that happens in a single browser, there are triggers coming from all of them. 23 | 24 | - gather information and evaluate the state as a whole - collect the events and data generated by each browser and assess if the order was correct with pseudo external consistency 25 | 26 | **why `piri-piri`? Well, to be honest, since I got to learn about SauceLabs in 2012 (during LXJS over some Nachos and Tabasco Hot Sauce), browser testing for me was always connected to spicy and sauce, so that inspired me to pick the one that is very famous on the portuguese cousine, that is, `piri-piri` :) 27 | 28 | # How to use it (API) 29 | 30 | ```JavaScript 31 | const pp = require('piri-piri') 32 | ``` 33 | 34 | ## Starting piri-piri 35 | 36 | piri-piri needs to get initialized in order to create a WebSockets server which will be used as the bridge to transfer messages between your Node.js process where tests and assertions are being run and the browsers that get instantiated. To start it, do: 37 | 38 | ```JavaScript 39 | pp.start((err) => { 40 | done() 41 | }) 42 | ``` 43 | 44 | ## Launching an instance 45 | 46 | In fact, what will get launched are headless electron processes. 47 | 48 | ```JavaScript 49 | pp.browser.spawn(, , (err) => { 50 | if (err) { } 51 | // .. 52 | }) 53 | ``` 54 | 55 | `scriptPath` is the path to the script that you want to run in the browser 56 | 57 | ## How should your script to run on the browser look like 58 | 59 | ```JavaScript 60 | const ppc = require('../../src').client 61 | 62 | module.exports = function (args) { 63 | ppc.connect((err, socket) => { // do this when you want to tell piri-piri that your app is ready 64 | if (err) { 65 | return console.log(err) 66 | } 67 | socket.on('exit', ppc.exit) 68 | }) 69 | } 70 | ``` 71 | 72 | ### Instruct a comamnd, a 'sum function' example 73 | 74 | browser app 75 | 76 | ```JavaScript 77 | const ppc = require('../../src').client 78 | 79 | module.exports = function (args) { 80 | ppc.handle('sum', (arr) => { 81 | var sum = Number(arr[0] + arr[1]) 82 | ppc.send(sum) 83 | }) 84 | 85 | ppc.connect((err) => { 86 | if (err) { 87 | return console.log(err) 88 | } 89 | }) 90 | } 91 | ``` 92 | 93 | tests side 94 | 95 | ```JavaScript 96 | pp.browser.spawn('./tests/scripts/method.js', 1, (err) => { 97 | if (err) { } 98 | var id = Object.keys(pp.clients)[0] // should be 0, electron is not being properly closed 99 | pp.browser.send(id, 'sum', 2, 2) 100 | setTimeout(() => { 101 | console.log(pp.clients[id].msgs[0]) // the 102 | pp.browser.send(id, 'exit') 103 | }, 500) 104 | }) 105 | ``` 106 | 107 | # Initial Development 108 | 109 | The initial development for this project was supported by INESC-ID, during the development of David Dias' M.Sc 110 | 111 | [![](https://img.shields.io/badge/INESC-GSD-brightgreen.svg?style=flat-square)](http://www.gsd.inesc-id.pt/) 112 | [![](https://img.shields.io/badge/TÉCNICO-LISBOA-blue.svg?style=flat-square)](http://tecnico.ulisboa.pt/) 113 | 114 | [![](https://cldup.com/pgZbzoshyV-3000x3000.png)](http://www.gsd.inesc-id.pt/) 115 | --------------------------------------------------------------------------------