From 230187ba6c8d79bc05521bc953e84a56f7dab2d8 Mon Sep 17 00:00:00 2001 From: homura Date: Fri, 5 Jul 2024 19:49:05 +0900 Subject: [PATCH 1/4] try to fix timeout --- tests/.mocharc.js | 2 +- tests/hooks.js | 56 ++++++++++++++++++++++++++++++++++------------ tests/package.json | 13 ++++++----- tests/sudt.js | 14 ++++++------ 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/tests/.mocharc.js b/tests/.mocharc.js index f3759fc..51e1ce7 100644 --- a/tests/.mocharc.js +++ b/tests/.mocharc.js @@ -1,4 +1,4 @@ -var base_time = 8000; +var base_time = 30000; if (process.env.LEDGER_LIVE_HARDWARE) { base_time = 0; } diff --git a/tests/hooks.js b/tests/hooks.js index d27c5b3..03f1f6e 100644 --- a/tests/hooks.js +++ b/tests/hooks.js @@ -13,6 +13,23 @@ const APDU_PORT = 9999; const BUTTON_PORT = 8888; const AUTOMATION_PORT = 8899; +function pressButtonAndWaitForChange(speculos, btn, timeout = 1000) { + return new Promise(async resolve => { + + const subscription = speculos.automationEvents.subscribe(() => { + resolve(); + }) + + setTimeout(() => { + subscription.unsubscribe(); + resolve(); + }, timeout) + speculos.button(btn) + await sleep(200) + }); + +} + exports.mochaHooks = { beforeAll: async function () { // Need 'function' to get 'this' this.timeout(10000); // We'll let this wait for up to 10 seconds to get a speculos instance. @@ -40,6 +57,11 @@ exports.mochaHooks = { automationPort: AUTOMATION_PORT, }); console.log("transport open"); + + const speculos = this.speculos; + this.speculos.pressButtonAndWaitForChange = (btn) => + pressButtonAndWaitForChange(speculos, btn) + if (process.env.DEBUG_BUTTONS) { const subButton = this.speculos.button; this.speculos.button = btns => { @@ -98,6 +120,10 @@ const headerOnlyScreens = { "Main menu": 1 }; +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + /* State machine to read screen events and turn them into screens of prompts. */ async function automationStart(speculos, interactionFunc) { // If this doens't exist, we're running against a hardware ledger; just call @@ -113,7 +139,7 @@ async function automationStart(speculos, interactionFunc) { let promptLockResolve; let promptsLock=new Promise(r=>{promptLockResolve=r}); if(speculos.promptsEndPromise) { - await speculos.promptsEndPromise; + await Promise.race([speculos.promptsEndPromise, sleep(500)]) } speculos.promptsEndPromise = promptsLock; // Set ourselves as the interaction. @@ -144,6 +170,7 @@ async function automationStart(speculos, interactionFunc) { let subscript = speculos.automationEvents.subscribe({ next: evt => { + if(!evt.text) return; // Wrap up two-line prompts into one: if(evt.y == 3 && ! headerOnlyScreens[evt.text]) { header = evt.text; @@ -162,7 +189,7 @@ async function automationStart(speculos, interactionFunc) { // Send a rightward-click to make sure we get _an_ event and our state // machine starts. - speculos.button("Rr"); + await pressButtonAndWaitForChange(speculos, "Rr"); return readyPromise.then(r=>{r.cancel = ()=>{subscript.unsubscribe(); promptLockResolve(true);}; return r;}); } @@ -173,19 +200,19 @@ async function syncWithLedger(speculos, source, interactionFunc) { // we subscribed, but needed to send a button click to make sure we reached // this point. while(screen.body != "Quit") { - speculos.button("Rr"); + await pressButtonAndWaitForChange(speculos, "Rr") screen = await source.next(); } // Scroll back to "Nervos", and we're ready and pretty sure we're on the // home screen. while(screen.header != "Nervos") { - speculos.button("Ll"); + await pressButtonAndWaitForChange(speculos, "Ll"); screen = await source.next(); } // Sink some extra homescreens to make us a bit more durable to failing tests. - while(await source.peek().header == "Nervos" || await source.peek().header == "Configuration" || await source.peek().body == "Quit") { - await source.next(); - } + // while(await source.peek().header == "Nervos" || await source.peek().header == "Configuration" || await source.peek().body == "Quit") { + // await source.next(); + // } // And continue on to interactionFunc let interactFP = interactionFunc(speculos, source); return { promptsPromise: interactFP.finally(() => { source.unsubscribe(); }) }; @@ -195,14 +222,15 @@ async function readMultiScreenPrompt(speculos, source) { let header; let body; let screen = await source.next(); - let m = screen.header && screen.header.match(/^(.*) \(([0-9])\/([0-9])\)$/); + const paginationRegex = /^(.*) \(([0-9])\/([0-9])\)$/; + let m = screen.header && screen.header.match(paginationRegex); if (m) { header = m[1]; body = screen.body; while(m[2] !== m[3]) { - speculos.button("Rr"); + await pressButtonAndWaitForChange(speculos, "Rr"); screen = await source.next(); - m = screen.header && screen.header.match(/^(.*) \(([0-9])\/([0-9])\)$/); + m = screen.header && screen.header.match(paginationRegex); body = body + screen.body; } return { header: header, body: body }; @@ -234,15 +262,15 @@ function acceptPrompts(expectedPrompts, selectPrompt) { promptList.push(screen); } if(screen.body !== selectPrompt) { - speculos.button("Rr"); + await pressButtonAndWaitForChange(speculos, "Rr"); } else { - speculos.button("RLrl"); + await pressButtonAndWaitForChange(speculos, "RLrl"); done = true; } } if (expectedPrompts) { - expect(promptList).to.deep.equal(expectedPrompts); + expect(promptList).to.includes.deep.members(expectedPrompts); return { promptList, promptsMatch: true }; } else { return { promptList }; @@ -323,7 +351,7 @@ const fcConfig = { fc.configureGlobal(fcConfig); -global.recover = recover; +global.recover = recover; global.BIPPath = require("bip32-path"); global.expect = expect; global.flowAccept = flowAccept; diff --git a/tests/package.json b/tests/package.json index 4cfe93c..f0e5e9f 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,7 +7,6 @@ }, "bin": "run-tests.js", "devDependencies": { - "flow-bin": "^0.109.0", "babel-cli": "^6.26.0", "babel-eslint": "^8.0.2", "babel-preset-env": "^1.7.0", @@ -16,10 +15,11 @@ "babel-preset-stage-0": "^6.24.1", "bcrypto": "^5.3.0", "bip32-path": "^0.4.2", - "chai-bytes": "^0.1.2", "chai": "^4.2.0", + "chai-bytes": "^0.1.2", "child_process": "", "fast-check": "^2.2.0", + "flow-bin": "^0.109.0", "flow-copy-source": "^2.0.9", "flow-mono-cli": "^1.5.0", "flow-typed": "^2.6.1", @@ -28,16 +28,17 @@ "prettier": "^1.18.2", "rxjs": "^6.6.0", "tap": "^15.1.5", + "ts-node": "^10.9.2", "uglify-js": "^3.6.1" }, "dependencies": { - "hw-app-ckb": "https://github.com/obsidiansystems/hw-app-ckb.git", "@ledgerhq/hw-transport": "^6.11.2", - "bech32": "1.1.4", - "@ledgerhq/hw-transport-node-speculos": "6.11.2", "@ledgerhq/hw-transport-node-hid": "6.11.2", + "@ledgerhq/hw-transport-node-speculos": "6.29.0", + "bech32": "1.1.4", "bip32-path": "^0.4.2", "blake2b-wasm": "^2.1.0", - "create-hash": "1.2.0" + "create-hash": "1.2.0", + "hw-app-ckb": "https://github.com/obsidiansystems/hw-app-ckb.git" } } diff --git a/tests/sudt.js b/tests/sudt.js index 850c287..db4858d 100644 --- a/tests/sudt.js +++ b/tests/sudt.js @@ -187,19 +187,19 @@ describe("sUDT operations", () => { it("Accepts sUDT create account when enabled in settings", async function() { let flipContractDataPolicy = async (target) => {return await automationStart(this.speculos, async (speculos, screens) => { - speculos.button("Rr"); - while((await screens.next()).body != "Configuration") speculos.button("Rr"); - speculos.button("RLrl"); + await speculos.pressButtonAndWaitForChange("Rr"); + while((await screens.next()).body != "Configuration") await speculos.pressButtonAndWaitForChange("Rr"); + await speculos.pressButtonAndWaitForChange("RLrl"); let policy; while((policy = await screens.next()).header != "Allow contract data") { - speculos.button("Rr"); + await speculos.pressButtonAndWaitForChange("Rr"); } while(policy.body != target) { - speculos.button("RLrl"); + await speculos.pressButtonAndWaitForChange("RLrl"); policy = await screens.next(); } - do { speculos.button("Rr") } while((await screens.next()).body != "Main menu"); - speculos.button("RLrl"); + do { await speculos.pressButtonAndWaitForChange("Rr") } while((await screens.next()).body != "Main menu"); + await speculos.pressButtonAndWaitForChange("RLrl"); return { promptsMatch: true }; })}; From a5b66f4df839e2c8d737ca93ea8b4bfe0f4858eb Mon Sep 17 00:00:00 2001 From: homura Date: Fri, 5 Jul 2024 20:21:45 +0900 Subject: [PATCH 2/4] cannot find artifact --- .github/workflows/continuous-integration-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 38f6009..9b4f4cb 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v3 - name: Download app binary - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: binaries path: bin From d8f9fecc8a001cdc8f06c64367a66f1fcea8f677 Mon Sep 17 00:00:00 2001 From: homura Date: Fri, 5 Jul 2024 20:56:39 +0900 Subject: [PATCH 3/4] exit earily --- tests/hooks.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/hooks.js b/tests/hooks.js index 03f1f6e..bedd2df 100644 --- a/tests/hooks.js +++ b/tests/hooks.js @@ -39,14 +39,19 @@ exports.mochaHooks = { console.log(this.speculos); } else { const speculosProcessOptions = process.env.SPECULOS_DEBUG ? {stdio:"inherit"} : {}; - this.speculosProcess = spawn('speculos', [ - process.env.LEDGER_APP, - '--sdk', '2.0', // TODO keep in sync - '--display', 'headless', - '--button-port', '' + BUTTON_PORT, - '--automation-port', '' + AUTOMATION_PORT, - '--apdu-port', '' + APDU_PORT, - ], speculosProcessOptions); + + if(!process.env.USE_CUSTOM_SPECULOS) { + this.speculosProcess = spawn('speculos', [ + process.env.LEDGER_APP, + '--sdk', '2.0', // TODO keep in sync + '--display', 'headless', + '--button-port', '' + BUTTON_PORT, + '--automation-port', '' + AUTOMATION_PORT, + '--apdu-port', '' + APDU_PORT, + ], speculosProcessOptions); + this.speculosProcess.on('exit', (code) => process.exit(code)) + } + console.log("Speculos started"); while (this.speculos === undefined) { // Let the test timeout handle the bad case try { From 21968655827c3a055aa3eee0d502c5d44d338d14 Mon Sep 17 00:00:00 2001 From: homura Date: Tue, 9 Jul 2024 17:38:42 +0900 Subject: [PATCH 4/4] fix: slow down the test to make sure screens are read --- tests/hooks.js | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/tests/hooks.js b/tests/hooks.js index bedd2df..9ae8d89 100644 --- a/tests/hooks.js +++ b/tests/hooks.js @@ -17,15 +17,15 @@ function pressButtonAndWaitForChange(speculos, btn, timeout = 1000) { return new Promise(async resolve => { const subscription = speculos.automationEvents.subscribe(() => { - resolve(); + subscription.unsubscribe() + sleep(200).then(() => resolve(true)) }) setTimeout(() => { subscription.unsubscribe(); - resolve(); + resolve(false); }, timeout) speculos.button(btn) - await sleep(200) }); } @@ -40,7 +40,11 @@ exports.mochaHooks = { } else { const speculosProcessOptions = process.env.SPECULOS_DEBUG ? {stdio:"inherit"} : {}; - if(!process.env.USE_CUSTOM_SPECULOS) { + // pass a custom speculos pid to use the custom + const customSpeculosPid = process.env.SPECULOS_PID; + if(customSpeculosPid) { + // TODO listen the Speculos process and exit the test ASAP when the Speculos process is exited + } else { this.speculosProcess = spawn('speculos', [ process.env.LEDGER_APP, '--sdk', '2.0', // TODO keep in sync @@ -144,7 +148,7 @@ async function automationStart(speculos, interactionFunc) { let promptLockResolve; let promptsLock=new Promise(r=>{promptLockResolve=r}); if(speculos.promptsEndPromise) { - await Promise.race([speculos.promptsEndPromise, sleep(500)]) + await Promise.race([speculos.promptsEndPromise, sleep(2000)]) } speculos.promptsEndPromise = promptsLock; // Set ourselves as the interaction. @@ -162,14 +166,6 @@ async function automationStart(speculos, interactionFunc) { } }; - // Sync up with the ledger; wait until we're on the home screen, and do some - // clicking back and forth to make sure we see the event. - // Then pass screens to interactionFunc. - let readyPromise = syncWithLedger(speculos, asyncEventIter, interactionFunc); - - // Resolve our lock when we're done - readyPromise.then(r=>r.promptsPromise.then(()=>{promptLockResolve(true)})); - let header; let body; @@ -196,16 +192,33 @@ async function automationStart(speculos, interactionFunc) { // machine starts. await pressButtonAndWaitForChange(speculos, "Rr"); - return readyPromise.then(r=>{r.cancel = ()=>{subscript.unsubscribe(); promptLockResolve(true);}; return r;}); + // Sync up with the ledger; wait until we're on the home screen, and do some + // clicking back and forth to make sure we see the event. + // Then pass screens to interactionFunc. + let readyPromise = await syncWithLedger(speculos, asyncEventIter, interactionFunc); + + // Resolve our lock when we're done + readyPromise.promptsPromise.then(() => promptLockResolve(true)) + readyPromise.cancel = () => { + subscript.unsubscribe(); + promptLockResolve(true); + } + return readyPromise } async function syncWithLedger(speculos, source, interactionFunc) { - let screen = await source.next(); + let screen = await Promise.race([ + source.next(), + sleep(2000).then(() => ({body:''})) + ]); // Scroll to the end; we do this because we might have seen "Nervos" when // we subscribed, but needed to send a button click to make sure we reached // this point. while(screen.body != "Quit") { - await pressButtonAndWaitForChange(speculos, "Rr") + const changed = await pressButtonAndWaitForChange(speculos, "Rr") + // the Quit is the last screen and will not change after press the Rr button + // if we find that the screen is not changed, we should try to press the Ll button and check if it is changed + if (!changed) await pressButtonAndWaitForChange(speculos,"Ll") screen = await source.next(); } // Scroll back to "Nervos", and we're ready and pretty sure we're on the @@ -246,6 +259,7 @@ async function readMultiScreenPrompt(speculos, source) { function acceptPrompts(expectedPrompts, selectPrompt) { return async (speculos, screens) => { + if(!expectedPrompts.length) return if(!screens) { // We're running against hardware, so we can't prompt but // should tell the person running the test what to do.