From 5c6e994af34e4219dce84a8c1f77725c9630b5f0 Mon Sep 17 00:00:00 2001 From: Rick Waldron Date: Thu, 18 Aug 2016 13:04:26 -0400 Subject: [PATCH] t2 restore: make api testable, match promise interface used throughout. Closes gh-493 - update-fetch.js made testable (TODO: more tests to be written) - restore: adds -f to skip device id check - restore: simplified controller handler - consolidate contents of "flash.js" into "restore.js" - restore.js: eliminate async, uses promises - restore.js: substantial tests, but still lacking coverage in a few areas, this can be done in a follow up - updates to jshintrc and bootstrap.js Signed-off-by: Rick Waldron --- bin/tessel-2.js | 7 +- lib/controller.js | 8 +- lib/flash.js | 180 --------------------- lib/tessel/restore.js | 243 ++++++++++++++++++++++++++-- lib/update-fetch.js | 110 ++++++------- test/.jshintrc | 1 + test/common/bootstrap.js | 1 + test/unit/restore.js | 337 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 630 insertions(+), 257 deletions(-) delete mode 100644 lib/flash.js create mode 100644 test/unit/restore.js diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 31a7a294..beab5b40 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -140,11 +140,16 @@ parser.command('provision') }) .help('Authorize your computer to control the USB-connected Tessel'); -parser.command('restore') +makeCommand('restore') .callback(options => { log.level(options.loglevel); callControllerWith('restoreTessel', options); }) + .option('force', { + abbr: 'f', + flag: true, + help: 'Skip the Device ID check and restore. Including this flag is not recommended, but may be necessary if Tessel memory device contents are corrupt.' + }) .help('Restore your Tessel by installing the factory version of OpenWrt.'); makeCommand('restart') diff --git a/lib/controller.js b/lib/controller.js index c73c11d9..82ae62a3 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -562,13 +562,7 @@ controller.restoreTessel = function(opts) { opts.altSetting = 1; return controller.get(opts).then((tessel) => { return new Promise((resolve, reject) => { - tessel.restore(opts) - .then((result) => { - resolve(result); - }) - .catch((err) => { - reject(err); - }); + return tessel.restore(opts).then(resolve).catch(reject); }); }); }; diff --git a/lib/flash.js b/lib/flash.js deleted file mode 100644 index 4cb4cb83..00000000 --- a/lib/flash.js +++ /dev/null @@ -1,180 +0,0 @@ -// System Objects -// ... - -// Third Party Dependencies -var async = require('async'); - -// Internal -var log = require('./log'); - -// Constants -var PAGE = 256; -var CHIP_ID = new Buffer([0x01, 0x02, 0x19]); - -function address(addr) { - return [(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF]; -} - -// Generate a mediatek factory partition -function factoryPartition(mac1, mac2) { - return toBuffer( - [0x20, 0x76, 0x03, 0x01], - mac1, - Array(30).fill(0xff), - mac2 - ); -} - -function transaction(usbConnection, write, read, status_poll, wren, next) { - read = read || 0; - status_poll = status_poll || false; - wren = wren || false; - - if (write.length > 500 || read >= Math.pow(2, 24)) { - return next(new Error('Transaction too large')); - } - var flags = (status_poll ? 1 : 0) | ((wren ? 1 : 0) << 1); - var hdr = [(read >> 0) & 0xff, (read >> 8) & 0xff, (read >> 16) & 0xff, flags]; - - var data = toBuffer(hdr, write); - - usbConnection.epOut.transfer(data, (err) => { - if (err) { - return next(err); - } - if (read > 0) { - return usbConnection.epIn.transfer(read, next); - } - next(); - }); -} - -function readChipId(usbConnection, next) { - transaction(usbConnection, [0x9f], 3, null, null, next); -} - -function checkChipId(chipId, next) { - if (Buffer.compare(chipId, CHIP_ID) !== 0) { - return next(new Error('Invalid chip ID (flash communication error)')); - } - next(); -} - -function setWriteEnabled(usbConnection, next) { - transaction(usbConnection, [0x06], null, null, null, next); -} - -function eraseChip(usbConnection, next) { - transaction(usbConnection, [0x60], null, null, null, next); -} - -// Poll for the WIP bit in the status register to go low -function waitTransactionComplete(usbConnection, next) { - setTimeout(function onWait() { - getStatus(usbConnection, (err, status) => { - if (err) { - return next(err); - } - if (status === 0) { - return next(); - } - process.nextTick(waitTransactionComplete, usbConnection, next); - }); - }, 200); -} - -// Read the status register -function getStatus(usbConnection, next) { - transaction(usbConnection, [0x05], 1, null, null, (err, data) => { - next(err, data ? data[0] : data); - }); -} - -function write(usbConnection, writeAddress, data, sliceStart, next) { - if (!next) { - next = sliceStart; - sliceStart = 0; - } - if (sliceStart >= data.length) { - return next(); - } - - var sliceEnd = sliceStart + PAGE; - var pageData = data.slice(sliceStart, sliceEnd); - writePage(usbConnection, writeAddress, pageData, (err) => { - if (err) { - return next(err); - } - process.nextTick(write, usbConnection, writeAddress + PAGE, data, sliceEnd, next); - }); -} - -function toBuffer() { - return Buffer.concat(Array.from(arguments).map( - (arg) => Array.isArray(arg) ? new Buffer(arg) : arg - )); -} - -// Write a page to flash -function writePage(usbConnection, addr, data, next) { - var status_poll = true; - var wren = true; - transaction( - usbConnection, - toBuffer([0x12], address(addr), data), - null, - status_poll, - wren, - next - ); -} - -// Returns a random integer, both low and high are inclusive -function randint(low, high) { - return Math.floor(Math.random() * (high - low + 1) + low); -} - -function randbyte() { - return randint(0, 255); -} - -function asyncLog(msg) { - return function onAsyncLog(next) { - log.info(msg); - next(); - }; -} - -module.exports = function flashDevice(usbConnection, ubootBuffer, squashfsBuffer) { - return new Promise((resolve, reject) => { - var uid = [randbyte(), randbyte(), randbyte(), randbyte()]; - var mac1 = [0x02, 0xa3].concat(uid); - var mac2 = [0x02, 0xa4].concat(uid); - - async.waterfall([ - asyncLog('Checking the chip id...'), - readChipId.bind(null, usbConnection), - checkChipId, - - asyncLog('Erasing the chip. This step takes about a minute...'), - setWriteEnabled.bind(null, usbConnection), - eraseChip.bind(null, usbConnection), - waitTransactionComplete.bind(null, usbConnection), - - asyncLog('Writing uboot...'), - write.bind(null, usbConnection, 0, ubootBuffer), - waitTransactionComplete.bind(null, usbConnection), - - asyncLog('Writing mediatek factory partition...'), - write.bind(null, usbConnection, 0x40000, factoryPartition(mac1, mac2)), - waitTransactionComplete.bind(null, usbConnection), - - asyncLog('Writing squashfs...'), - write.bind(null, usbConnection, 0x50000, squashfsBuffer), - waitTransactionComplete.bind(null, usbConnection), - asyncLog('The update was successful.'), - asyncLog('Please power cycle your Tessel.') - - ], (err) => err ? reject(err) : resolve()); - }); -}; diff --git a/lib/tessel/restore.js b/lib/tessel/restore.js index 80ea416c..0f285e57 100644 --- a/lib/tessel/restore.js +++ b/lib/tessel/restore.js @@ -5,24 +5,243 @@ // ... // Internal +var log = require('../log'); var Tessel = require('./tessel'); var update = require('../update-fetch'); -var log = require('../log'); -var flash = require('../flash'); -Tessel.prototype.restore = function restore() { - var usbConnection = this.connection; +// Datasheet Reference: +// http://www.cypress.com/file/177966/download +// +// +// Constants + +// 7.5.1 Status Register 1 (SR1) +// - Write Enable (WREN 06h) +// p. 48 +// 8.2 Write Enable Command (WREN) +// p. 57 +const COMMAND_WREN = 0x06; + +// 9.2.2 Read Identification (RDID 9Fh) +// p. 71 +const COMMAND_RDID = 0x9F; + +// 9.3.1 Read Status Register-1 (RDSR1 05h) +// p. 72 +const COMMAND_RDSR1 = 0x05; + +// 9.6.3 Bulk Erase (BE 60h or C7h) +// p. 105 +const COMMAND_BE = 0x60; + +// 9.5.1.1 Page Programming +// p. 98 +const PAGE_SIZE = 256; + +// TODO: Find reference +const EXPECTED_RDID = '010219'; + +// TODO: Find reference +const MAX_READ_SIZE = Math.pow(2, 24); + + +Tessel.prototype.restore = function(options) { + return new Promise((resolve, reject) => { + var rdid = Promise.resolve(); + + // If no "force/-f" flag is present, then + // we must validate the device id. This is the + // default behavior. Only extreme circumstances + // call for forcing the restore process. + if (!options.force) { + rdid = exportables.validateDeviceId(this.usbConnection); + } + + // 1. Download Images for download images + // 2. Flash images to USB connected Tessel 2 + return rdid.then(update.fetchRestore).then(images => { + return exportables.flash(this, images).then(resolve).catch(reject); + }); + }); +}; + +// Contains functionality that must be stubbable in tests +var exportables = {}; + +function address(addr) { + return [(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF]; +} + +function toBuffer() { + return Buffer.concat(Array.from(arguments).map( + (arg) => Array.isArray(arg) ? new Buffer(arg) : arg + )); +} + +function randUint8() { + return Math.round(Math.random() * 255); +} + +// Generate a mediatek factory partition +// TODO: Find reference +exportables.partition = function(mac1, mac2) { + return toBuffer( + [0x20, 0x76, 0x03, 0x01], + mac1, + Array(30).fill(0xFF), + mac2 + ); +}; + +exportables.transaction = function(usb, bytesOrCommand, readLength, statusPoll, writeEnable) { + readLength = readLength || 0; + statusPoll = statusPoll || false; + writeEnable = writeEnable || false; + + if (typeof bytesOrCommand === 'number') { + bytesOrCommand = [bytesOrCommand]; + } + + if (bytesOrCommand.length > 500 || readLength >= MAX_READ_SIZE) { + return Promise.reject(new Error('Transaction too large')); + } + + var flags = Number(statusPoll) | (Number(writeEnable) << 1); + var hdr = [(readLength >> 0) & 0xFF, (readLength >> 8) & 0xFF, (readLength >> 16) & 0xFF, flags]; + var data = toBuffer(hdr, bytesOrCommand); + + return new Promise((resolve, reject) => { + usb.epOut.transfer(data, (error) => { + if (error) { + return reject(error); + } + if (readLength > 0) { + usb.epIn.transfer(readLength, (error, data) => { + if (error) { + return reject(error); + } + return resolve(data); + }); + } else { + return resolve(); + } + }); + }); +}; + +exportables.validateDeviceId = function(usb) { return new Promise((resolve, reject) => { + return exportables.transaction(usb, COMMAND_RDID, 3).then(buffer => { + if (buffer.toString('hex') !== EXPECTED_RDID) { + return reject(new Error('Invalid Device ID (Flash Memory Communication Error)')); + } + return resolve(); + }); + }); +}; + +// 9.6.3 Bulk Erase (BE 60h or C7h) +exportables.bulkEraseFlash = function(usb) { + return exportables.transaction(usb, COMMAND_BE); +}; + +// NOTE: The following commands do not directly interact with the flash memory registers +// described in the cited sections. The register is actually read in firmare/flash.c +// (https://github.com/tessel/t2-firmware/blob/1d3e13931d9d668013e5446330c74faa09477c17/firmware/flash.c#L3-L22 ) + +// 8.2 Write Enable Command +// p. 57 +exportables.enableWrite = function(usb) { + return exportables.transaction(usb, COMMAND_WREN); +}; + +// 9.3.1 Read Status Register-1 (RDSR1 05h) +// p. 72 +exportables.status = function(usb) { + return exportables.transaction(usb, COMMAND_RDSR1, 1).then(buffer => { + return Promise.resolve(buffer ? buffer[0] : buffer); + }); +}; + +// Status Register: Poll until WIP bit reports 0 +exportables.waitTransactionComplete = function(usb) { + return new Promise(resolve => { + var poll = () => { + exportables.status(usb).then(status => { + // If "status" is anything but 0, keep checking. + if (status) { + return poll(); + } + resolve(); + }); + }; + poll(); + }); +}; - log.info('Proceeding with updating OpenWrt...'); +exportables.write = function(usb, offset, buffer) { + return new Promise(resolve => { + var sendChunk = () => { + var size = buffer.length > PAGE_SIZE ? PAGE_SIZE : buffer.length; + exportables.writePage(usb, offset, buffer.slice(0, size)).then(() => { + buffer = buffer.slice(size); + offset += PAGE_SIZE; - // download images - return update - .fetchRestore() - .then((result) => { - flash(usbConnection, result.uboot, result.squashfs) - .then(resolve) - .catch(reject); + // If "buffer" still has contents, keep sending chunks. + if (buffer.length) { + sendChunk(); + } else { + resolve(); + } }); + }; + sendChunk(); + }).then(() => exportables.waitTransactionComplete(usb)); +}; + +// Write a 256 Byte Page to Flash +exportables.writePage = function(usb, start, page) { + var buffer = toBuffer([0x12], address(start), page); + return exportables.transaction(usb, buffer, 0, true, true); +}; + +exportables.flash = function(tessel, buffers) { + var usb = tessel.usbConnection; + + log.info('Restoring your Tessel...'); + + return new Promise((resolve, reject) => { + var uid = [randUint8(), randUint8(), randUint8(), randUint8()]; + + // 9.5.2 Page Program (PP 02h or 4PP 12h) + var mac1 = [0x02, 0xA3].concat(uid); + var mac2 = [0x02, 0xA4].concat(uid); + + buffers.partition = exportables.partition(mac1, mac2); + + return Promise.resolve().then(() => { + log.info('Bulk Erasing Flash Memory...'); + return exportables.bulkEraseFlash(usb); + }) + .then(() => { + log.info('Writing U-Boot...'); + return exportables.write(usb, 0, buffers.uboot); + }) + .then(() => { + log.info('Writing MediaTek factory partition...'); + return exportables.write(usb, 0x40000, buffers.partition); + }) + .then(() => { + log.info('Writing OpenWRT SquashFS (this may take a few minutes)...'); + return exportables.write(usb, 0x50000, buffers.squashfs); + }) + .then(() => { + log.info('Restore successful. Please reboot your Tessel'); + return resolve(); + }).catch(reject); }); }; + +if (global.IS_TEST_ENV) { + module.exports = exportables; +} diff --git a/lib/update-fetch.js b/lib/update-fetch.js index f7cb077b..94220b2e 100644 --- a/lib/update-fetch.js +++ b/lib/update-fetch.js @@ -24,20 +24,27 @@ const RESTORE_TGZ_URL = 'https://s3.amazonaws.com/builds.tessel.io/custom/new_bu const RESTORE_UBOOT_FILE = 'openwrt-ramips-mt7620-Default-u-boot.bin'; const RESTORE_SQASHFS_FILE = 'openwrt-ramips-mt7620-tessel-squashfs-sysupgrade.bin'; +var exportables = { + OPENWRT_BINARY_FILE, + FIRMWARE_BINARY_FILE, + RESTORE_UBOOT_FILE, + RESTORE_SQASHFS_FILE, +}; + /* Requests a list of available builds from the build server. Returns list of build names in a Promise. */ -function requestBuildList() { - return new Promise(function(resolve, reject) { +exportables.requestBuildList = function() { + return new Promise((resolve, reject) => { // Fetch the list of available builds - request.get(BUILDS_JSON_FILE, function(err, response, body) { + request.get(BUILDS_JSON_FILE, (err, response, body) => { if (err) { return reject(err); } - var outcome = reviewResponse(response); + var outcome = exportables.reviewResponse(response); var builds; // If there wasn't an issue with the request if (outcome.success) { @@ -59,18 +66,18 @@ function requestBuildList() { } }); }); -} +}; -function loadLocalBinaries(opts) { +exportables.loadLocalBinaries = function(options) { var openwrtUpdateLoad = Promise.resolve(new Buffer(0)); var firmwareUpdateLoad = Promise.resolve(new Buffer(0)); - if (opts['openwrt-path']) { - openwrtUpdateLoad = module.exports.loadLocalBinary(opts['openwrt-path']); + if (options['openwrt-path']) { + openwrtUpdateLoad = exportables.loadLocalBinary(options['openwrt-path']); } - if (opts['firmware-path']) { - firmwareUpdateLoad = module.exports.loadLocalBinary(opts['firmware-path']); + if (options['firmware-path']) { + firmwareUpdateLoad = exportables.loadLocalBinary(options['firmware-path']); } return Promise.all([openwrtUpdateLoad, firmwareUpdateLoad]) @@ -84,51 +91,48 @@ function loadLocalBinaries(opts) { }; } }); -} +}; // Reads a binary from a local path -function loadLocalBinary(path) { +exportables.loadLocalBinary = function(path) { return new Promise((resolve, reject) => { - fs.readFile(path, (err, binary) => { - if (err) { - return reject(err); + fs.readFile(path, (error, binary) => { + if (error) { + return reject(error); } else { resolve(binary); } }); }); -} +}; -function fetchRestoreFiles() { - var fileMap = { +exportables.fetchRestore = function() { + return exportables.downloadTgz(RESTORE_TGZ_URL, { uboot: RESTORE_UBOOT_FILE, squashfs: RESTORE_SQASHFS_FILE - }; - return downloadTgz(RESTORE_TGZ_URL, fileMap); -} + }); +}; /* Accepts a build name and attempts to fetch the build images from the server. Returns build contents in a Promise */ -function fetchBuild(build) { - var tgzUrl = urljoin(FIRMWARE_PATH, build.sha + '.tar.gz'); - var fileMap = { +exportables.fetchBuild = function(build) { + return exportables.downloadTgz(urljoin(FIRMWARE_PATH, `${build.sha}.tar.gz`), { firmware: FIRMWARE_BINARY_FILE, openwrt: OPENWRT_BINARY_FILE - }; - return downloadTgz(tgzUrl, fileMap); -} + }); +}; -function downloadTgz(tgzUrl, fileMap) { - return new Promise(function(resolve, reject) { - log.info('Beginning update download. This could take a couple minutes..'); +exportables.downloadTgz = function(tgzUrl, fileMap) { + return new Promise((resolve, reject) => { + log.info('Downloading files. This may take a few minutes...'); var files = {}; // Fetch the list of available files - extract.on('entry', function(header, stream, callback) { + extract.on('entry', (header, stream, callback) => { // The buffer to save incoming data to // The filename of this entry var tgzFilename = path.basename(header.name); @@ -136,7 +140,7 @@ function downloadTgz(tgzUrl, fileMap) { for (var key in fileMap) { var expectedFilename = fileMap[key]; if (tgzFilename === expectedFilename) { - return streamToBuffer(stream, function(err, buffer) { + return streamToBuffer(stream, (error, buffer) => { files[key] = buffer; callback(); }); @@ -145,11 +149,11 @@ function downloadTgz(tgzUrl, fileMap) { callback(); }); - extract.once('finish', function() { + extract.once('finish', () => { for (var key in files) { var file = files[key]; if (!file.length) { - return reject(new Error('Fetched file wasn\'t formatted properly.')); + return reject(new Error('Fetched file was not formatted properly.')); } } log.info('Download complete!'); @@ -159,21 +163,21 @@ function downloadTgz(tgzUrl, fileMap) { var req = request.get(tgzUrl); // When we receive the response - req.on('response', function(res) { + req.on('response', (res) => { // Parse out the length of the incoming bundle - var len = parseInt(res.headers['content-length'], 10); + var contentLength = parseInt(res.headers['content-length'], 10); // Create a new progress bar var bar = new ProgressBar(' Downloading [:bar] :percent :etas remaining', { complete: '=', incomplete: ' ', width: 20, - total: len + total: contentLength }); // When we get incoming data, update the progress bar - res.on('data', function(chunk) { + res.on('data', (chunk) => { bar.tick(chunk.length); }); @@ -181,9 +185,9 @@ function downloadTgz(tgzUrl, fileMap) { res.pipe(gunzip).pipe(extract); }); }); -} +}; -function reviewResponse(response) { +exportables.reviewResponse = function(response) { var outcome = { success: true }; @@ -191,23 +195,15 @@ function reviewResponse(response) { // If there was an issue with the server endpoint, reject if (response.statusCode !== 200) { outcome.success = false; - outcome.reason = 'Invalid status code on build server request: ' + response.statusCode; + outcome.reason = `Invalid status code on build server request: ${response.statusCode}`; } return outcome; -} - -function findBuild(builds, property, value) { - return builds.filter(function(build) { - return build[property] === value; - })[0]; -} - -module.exports.requestBuildList = requestBuildList; -module.exports.fetchBuild = fetchBuild; -module.exports.findBuild = findBuild; -module.exports.loadLocalBinaries = loadLocalBinaries; -module.exports.loadLocalBinary = loadLocalBinary; -module.exports.fetchRestore = fetchRestoreFiles; -module.exports.OPENWRT_BINARY_FILE = OPENWRT_BINARY_FILE; -module.exports.FIRMWARE_BINARY_FILE = FIRMWARE_BINARY_FILE; +}; + +exportables.findBuild = function(builds, property, value) { + return builds.find(build => build[property] === value); +}; + + +module.exports = exportables; diff --git a/test/.jshintrc b/test/.jshintrc index 9f47c345..b67a3ad5 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -75,6 +75,7 @@ "Project": true, "Promise": true, "provision": true, + "restore": true, "reference": true, "RemoteProcessSimulator": true, "request": true, diff --git a/test/common/bootstrap.js b/test/common/bootstrap.js index 5626b45d..96762164 100644 --- a/test/common/bootstrap.js +++ b/test/common/bootstrap.js @@ -51,6 +51,7 @@ global.deploy = require('../../lib/tessel/deploy'); global.deployment = require('../../lib/tessel/deployment/index'); global.glob = require('../../lib/tessel/deployment/glob'); global.provision = require('../../lib/tessel/provision'); +global.restore = require('../../lib/tessel/restore'); global.RSA = require('../../lib/tessel/rsa-delegation'); // ./lib/* diff --git a/test/unit/restore.js b/test/unit/restore.js new file mode 100644 index 00000000..07810dc8 --- /dev/null +++ b/test/unit/restore.js @@ -0,0 +1,337 @@ +// Test dependencies are required and exposed in common/bootstrap.js +require('../common/bootstrap'); + +exports['Tessel.prototype.restore'] = { + setUp(done) { + this.sandbox = sinon.sandbox.create(); + this.spinnerStart = this.sandbox.stub(log.spinner, 'start'); + this.spinnerStop = this.sandbox.stub(log.spinner, 'stop'); + this.warn = this.sandbox.stub(log, 'warn'); + this.info = this.sandbox.stub(log, 'info'); + this.images = { + uboot: new Buffer('uboot'), + squashfs: new Buffer('squashfs'), + }; + this.status = this.sandbox.stub(restore, 'status', () => Promise.resolve(0)); + this.fetchRestore = this.sandbox.stub(updates, 'fetchRestore', () => { + return Promise.resolve(this.images); + }); + this.restore = this.sandbox.spy(Tessel.prototype, 'restore'); + this.tessel = TesselSimulator(); + + done(); + }, + + tearDown(done) { + this.tessel.mockClose(); + this.sandbox.restore(); + done(); + }, + + restoreWithValidateDeviceId(test) { + test.expect(2); + + this.validateDeviceId = this.sandbox.stub(restore, 'validateDeviceId', () => Promise.resolve()); + this.transaction = this.sandbox.stub(restore, 'transaction', () => Promise.resolve()); + + this.tessel.restore({}) + .then(() => { + test.equal(this.validateDeviceId.callCount, 1); + test.equal(this.fetchRestore.callCount, 1); + test.done(); + }); + }, + + restoreWithoutValidateDeviceId(test) { + test.expect(2); + + this.validateDeviceId = this.sandbox.stub(restore, 'validateDeviceId', () => Promise.resolve()); + this.transaction = this.sandbox.stub(restore, 'transaction', () => Promise.resolve()); + + this.tessel.restore({ + force: true + }) + .then(() => { + test.equal(this.validateDeviceId.callCount, 0); + test.equal(this.fetchRestore.callCount, 1); + test.done(); + }); + }, + + restoreFetchImages(test) { + test.expect(3); + + this.flash = this.sandbox.stub(restore, 'flash', () => Promise.resolve()); + this.transaction = this.sandbox.stub(restore, 'transaction', (usb, bytesOrCommand) => { + if (bytesOrCommand === 0x9F) { + return Promise.resolve(new Buffer([0x01, 0x02, 0x19])); + } + + return Promise.resolve(); + }); + + this.tessel.restore({}) + .then(() => { + test.equal(this.fetchRestore.callCount, 1); + test.equal(this.flash.callCount, 1); + test.equal(this.flash.lastCall.args[1], this.images); + test.done(); + }); + }, +}; + +exports['restore.*'] = { + setUp(done) { + this.sandbox = sinon.sandbox.create(); + this.spinnerStart = this.sandbox.stub(log.spinner, 'start'); + this.spinnerStop = this.sandbox.stub(log.spinner, 'stop'); + this.warn = this.sandbox.stub(log, 'warn'); + this.info = this.sandbox.stub(log, 'info'); + this.images = { + uboot: new Buffer('uboot'), + squashfs: new Buffer('squashfs'), + }; + this.status = this.sandbox.stub(restore, 'status', () => Promise.resolve(0)); + this.fetchRestore = this.sandbox.stub(updates, 'fetchRestore', () => { + return Promise.resolve(this.images); + }); + this.restore = this.sandbox.spy(Tessel.prototype, 'restore'); + this.tessel = TesselSimulator(); + + + done(); + }, + + tearDown(done) { + this.tessel.mockClose(); + this.sandbox.restore(); + done(); + }, + + validateDeviceIdSuccess(test) { + test.expect(1); + + this.transaction = this.sandbox.stub(restore, 'transaction', () => Promise.resolve(new Buffer([0x01, 0x02, 0x19]))); + + restore.validateDeviceId({}) + .then(() => { + test.equal(this.transaction.callCount, 1); + test.done(); + }); + }, + + validateDeviceIdFailure(test) { + test.expect(1); + + this.transaction = this.sandbox.stub(restore, 'transaction', () => Promise.resolve(new Buffer([0x00, 0x00, 0x00]))); + + restore.validateDeviceId({}) + .catch((error) => { + test.equal(error.message, 'Invalid Device ID (Flash Memory Communication Error)'); + test.done(); + }); + }, + + partitionReturnsBuffer(test) { + test.expect(1); + // TODO: we need more specific tests for this + test.equal(Buffer.isBuffer(restore.partition([1], [2])), true); + test.done(); + }, + + partitionLayout(test) { + test.expect(18); + + var uid = [randUint8(), randUint8(), randUint8(), randUint8()]; + var mac1 = [0x02, 0xA3].concat(uid); + var mac2 = [0x02, 0xA4].concat(uid); + + var partition = restore.partition(mac1, mac2); + + test.equal(partition.length, 46); + + // TODO: Find reference + test.equal(partition[0], 0x20); + test.equal(partition[1], 0x76); + test.equal(partition[2], 0x03); + test.equal(partition[3], 0x01); + + // mac1 + test.equal(partition[4], 0x02); + test.equal(partition[5], 0xA3); + test.equal(partition[6], uid[0]); + test.equal(partition[7], uid[1]); + test.equal(partition[8], uid[2]); + test.equal(partition[9], uid[3]); + + // Next portion is 30 bytes, all 0xFF + test.deepEqual(partition.slice(10, 40), Array(30).fill(0xFF)); + + // mac2 + test.equal(partition[40], 0x02); + test.equal(partition[41], 0xA4); + test.equal(partition[42], uid[0]); + test.equal(partition[43], uid[1]); + test.equal(partition[44], uid[2]); + test.equal(partition[45], uid[3]); + + test.done(); + }, + +}; + +exports['restore.transaction'] = { + setUp(done) { + this.sandbox = sinon.sandbox.create(); + this.spinnerStart = this.sandbox.stub(log.spinner, 'start'); + this.spinnerStop = this.sandbox.stub(log.spinner, 'stop'); + this.warn = this.sandbox.stub(log, 'warn'); + this.info = this.sandbox.stub(log, 'info'); + this.images = { + uboot: new Buffer('uboot'), + squashfs: new Buffer('squashfs'), + }; + this.status = this.sandbox.stub(restore, 'status', () => Promise.resolve(0)); + this.fetchRestore = this.sandbox.stub(updates, 'fetchRestore', () => { + return Promise.resolve(this.images); + }); + this.restore = this.sandbox.spy(Tessel.prototype, 'restore'); + this.tessel = TesselSimulator(); + + this.usb = new USB.Connection({}); + this.usb.epOut = new Emitter(); + this.usb.epOut.transfer = this.sandbox.spy((data, callback) => { + callback(null); + }); + + this.usb.epIn = new Emitter(); + this.usb.epIn.transfer = this.sandbox.spy((data, callback) => { + callback(null, this.usb.epIn._mockbuffer); + }); + this.usb.epIn._mockdata = new Buffer('mockbuffer'); + + this.expectedBuffer = new Buffer([0x00, 0x00, 0x00, 0x00, 0xFF]); + + done(); + }, + + tearDown(done) { + this.tessel.mockClose(); + this.sandbox.restore(); + done(); + }, + + transactionAcceptsCommandNumber(test) { + test.expect(2); + + restore.transaction(this.usb, 0xFF).then(() => { + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + test.equal(this.usb.epIn.transfer.callCount, 0); + test.done(); + }); + }, + + transactionAcceptsArray(test) { + test.expect(2); + + restore.transaction(this.usb, [0xFF]).then(() => { + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + test.equal(this.usb.epIn.transfer.callCount, 0); + test.done(); + }); + }, + + transactionAcceptsBuffer(test) { + test.expect(2); + + restore.transaction(this.usb, new Buffer([0xFF])).then(() => { + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + test.equal(this.usb.epIn.transfer.callCount, 0); + test.done(); + }); + }, + + transactionWithReadlength(test) { + test.expect(4); + + this.expectedBuffer[0] = 32; + + restore.transaction(this.usb, 0xFF, 32).then(() => { + test.equal(this.usb.epOut.transfer.callCount, 1); + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + + test.equal(this.usb.epIn.transfer.callCount, 1); + test.equal(this.usb.epIn.transfer.lastCall.args[0], 32); + test.done(); + }); + }, + + transactionWithReadlengthStatusPoll(test) { + test.expect(4); + + this.expectedBuffer[0] = 32; + this.expectedBuffer[3] = 0b00000001; + + restore.transaction(this.usb, 0xFF, 32, true).then(() => { + test.equal(this.usb.epOut.transfer.callCount, 1); + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + + test.equal(this.usb.epIn.transfer.callCount, 1); + test.equal(this.usb.epIn.transfer.lastCall.args[0], 32); + test.done(); + }); + }, + + transactionWithReadlengthStatusPollWriteEnable(test) { + test.expect(4); + + this.expectedBuffer[0] = 32; + this.expectedBuffer[3] = 0b00000011; + + restore.transaction(this.usb, 0xFF, 32, true, true).then(() => { + test.equal(this.usb.epOut.transfer.callCount, 1); + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + + test.equal(this.usb.epIn.transfer.callCount, 1); + test.equal(this.usb.epIn.transfer.lastCall.args[0], 32); + test.done(); + }); + }, + + transactionStatusPollWithoutReadlength(test) { + test.expect(3); + + this.expectedBuffer[0] = 0; + this.expectedBuffer[3] = 0b00000001; + + restore.transaction(this.usb, 0xFF, 0, true).then(() => { + test.equal(this.usb.epOut.transfer.callCount, 1); + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + + test.equal(this.usb.epIn.transfer.callCount, 0); + test.done(); + }); + }, + + transactionStatusPollWriteEnableWithoutReadlength(test) { + test.expect(3); + + this.expectedBuffer[0] = 0; + this.expectedBuffer[3] = 0b00000011; + + restore.transaction(this.usb, 0xFF, 0, true, true).then(() => { + test.equal(this.usb.epOut.transfer.callCount, 1); + test.equal(this.usb.epOut.transfer.lastCall.args[0].equals(this.expectedBuffer), true); + + test.equal(this.usb.epIn.transfer.callCount, 0); + test.done(); + }); + }, +}; + +function randUint8() { + return Math.round(Math.random() * 255); +} + + +// TODO: Needs tests for restore.write, will add in follow up