Skip to content

Commit b9cd571

Browse files
authored
feat: Add concurrency for running tests (#243)
Running the integration tests can be speed up significantly by adding concurrency. This adds a `—concurrency` and `-c` option to the cli that can be used to run multiple tests in parallel.
1 parent 6d58047 commit b9cd571

File tree

3 files changed

+100
-27
lines changed

3 files changed

+100
-27
lines changed

modules/integration-node/src/cli.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,24 @@ const cli = yargs
5757
describe: 'an optional test name to execute',
5858
type: 'string'
5959
})
60+
.option('concurrency', {
61+
alias: 'c',
62+
describe: 'an optional concurrency for running tests',
63+
type: 'number',
64+
default: 1
65+
})
6066
.demandCommand()
6167

6268
;(async (argv) => {
63-
const { _: [ command ], tolerateFailures, testName } = argv
69+
const { _: [ command ], tolerateFailures, testName, concurrency } = argv
6470
/* I set the result to 1 so that if I fall through the exit condition is a failure */
6571
let result = 1
6672
if (command === 'decrypt') {
6773
const { vectorFile } = argv as unknown as { vectorFile: string}
68-
result = await integrationDecryptTestVectors(vectorFile, tolerateFailures, testName)
74+
result = await integrationDecryptTestVectors(vectorFile, tolerateFailures, testName, concurrency)
6975
} else if (command === 'encrypt') {
7076
const { manifestFile, keyFile, decryptOracle } = argv as unknown as { manifestFile: string, keyFile: string, decryptOracle: string}
71-
result = await integrationEncryptTestVectors(manifestFile, keyFile, decryptOracle, tolerateFailures, testName)
77+
result = await integrationEncryptTestVectors(manifestFile, keyFile, decryptOracle, tolerateFailures, testName, concurrency)
7278
} else {
7379
console.log(`Unknown command ${command}`)
7480
cli.showHelp()

modules/integration-node/src/integration_tests.ts

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,57 +72,124 @@ export async function testEncryptVector ({ name, keysInfo, encryptOp, plainTextD
7272
}
7373
}
7474

75-
export async function integrationDecryptTestVectors (vectorFile: string, tolerateFailures: number = 0, testName?: string) {
75+
export async function integrationDecryptTestVectors (vectorFile: string, tolerateFailures: number = 0, testName?: string, concurrency: number = 1) {
7676
const tests = await getDecryptTestVectorIterator(vectorFile)
77-
let failureCount = 0
78-
for (const test of tests) {
77+
78+
return parallelTests(concurrency, tolerateFailures, runTest, tests)
79+
80+
async function runTest (test: TestVectorInfo): Promise<boolean> {
7981
if (testName) {
80-
if (test.name !== testName) continue
82+
if (test.name !== testName) return true
8183
}
8284
const { result, name, err } = await testDecryptVector(test)
8385
if (result) {
8486
console.log({ name, result })
87+
return true
8588
} else {
8689
if (err && notSupportedDecryptMessages.includes(err.message)) {
8790
console.log({ name, result: `Not supported: ${err.message}` })
88-
continue
91+
return true
8992
}
9093
console.log({ name, result, err })
91-
}
92-
if (!result) {
93-
failureCount += 1
94-
if (!tolerateFailures) return failureCount
95-
tolerateFailures--
94+
return false
9695
}
9796
}
98-
return failureCount
9997
}
10098

101-
export async function integrationEncryptTestVectors (manifestFile: string, keyFile: string, decryptOracle: string, tolerateFailures: number = 0, testName?: string) {
99+
export async function integrationEncryptTestVectors (manifestFile: string, keyFile: string, decryptOracle: string, tolerateFailures: number = 0, testName?: string, concurrency: number = 1) {
102100
const decryptOracleUrl = new URL(decryptOracle)
103101
const tests = await getEncryptTestVectorIterator(manifestFile, keyFile)
104-
let failureCount = 0
105-
for (const test of tests) {
102+
103+
return parallelTests(concurrency, tolerateFailures, runTest, tests)
104+
105+
async function runTest (test: EncryptTestVectorInfo): Promise<boolean> {
106106
if (testName) {
107-
if (test.name !== testName) continue
107+
if (test.name !== testName) return true
108108
}
109109
const { result, name, err } = await testEncryptVector(test, decryptOracleUrl)
110110
if (result) {
111111
console.log({ name, result })
112+
return true
112113
} else {
113114
if (err && notSupportedEncryptMessages.includes(err.message)) {
114115
console.log({ name, result: `Not supported: ${err.message}` })
115-
continue
116+
return true
116117
}
117118
console.log({ name, result, err })
119+
return false
118120
}
119-
if (!result) {
120-
failureCount += 1
121-
if (!tolerateFailures) return failureCount
122-
tolerateFailures--
123-
}
124121
}
125-
return failureCount
122+
}
123+
124+
async function parallelTests<
125+
Test extends EncryptTestVectorInfo|TestVectorInfo,
126+
work extends (test: Test) => Promise<boolean>
127+
>(max: number, tolerateFailures: number, runTest: work, tests: IterableIterator<Test>) {
128+
let _resolve: (failureCount: number) => void
129+
const queue = new Set<Promise<void>>()
130+
let failureCount = 0
131+
132+
return new Promise<number>((resolve) => {
133+
_resolve = resolve
134+
enqueue()
135+
})
136+
137+
function enqueue (): void {
138+
/* If there are more failures than I am willing to tolerate, stop. */
139+
if (failureCount > tolerateFailures) return _resolve(failureCount)
140+
/* Do not over-fill the queue! */
141+
if (queue.size > max) return
142+
143+
const { value, done } = tests.next()
144+
/* There is an edge here,
145+
* you _could_ return a value *and* be done.
146+
* Most iterators don't but in this case
147+
* we just process the value and ask for another.
148+
* Which will return done as true again.
149+
*/
150+
if (!value && done) return _resolve(failureCount)
151+
152+
/* I need to define the work to be enqueue
153+
* and a way to dequeue this work when complete.
154+
* A Set of promises works nicely.
155+
* Hold the variable here
156+
* put it in the Set, take it out, and Bob's your uncle.
157+
*/
158+
const work: Promise<void> = runTest(value)
159+
.then((pass: boolean) => {
160+
if (!pass) failureCount += 1
161+
})
162+
/* If there is some unknown error,
163+
* it's just an error...
164+
* Treat it like a test failure.
165+
*/
166+
.catch((err) => {
167+
console.log(err)
168+
failureCount += 1
169+
})
170+
.then(() => {
171+
/* Dequeue this work. */
172+
queue.delete(work)
173+
/* More to eat? */
174+
enqueue()
175+
})
176+
177+
/* Enqueue this work */
178+
queue.add(work)
179+
180+
/* Fill the queue.
181+
* The over-fill check above protects me.
182+
* Sure, it is possible to exceed the stack depth.
183+
* If you are trying to run ~10K tests in parallel
184+
* on a system where that is actually faster,
185+
* I want to talk to you.
186+
* It is true that node can be configured to process
187+
* > 10K HTTP requests no problem,
188+
* but even the decrypt tests require that you first
189+
* encrypt something locally before making the http call.
190+
*/
191+
enqueue()
192+
}
126193
}
127194

128195
interface TestVectorResults {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
"integration-browser-decrypt": "npm run build; lerna run build_fixtures --stream --no-prefix -- -- decrypt -v $npm_package_config_localTestVectors --karma",
3232
"integration-browser-encrypt": "npm run build; lerna run build_fixtures --stream --no-prefix -- -- encrypt -m $npm_package_config_encryptManifestList -k $npm_package_config_encryptKeyManifest -o $npm_package_config_decryptOracle --karma",
3333
"browser-integration": "run-s integration-browser-*",
34-
"integration-node-decrypt": "npm run build; lerna run integration_node --stream --no-prefix -- -- decrypt -v $npm_package_config_localTestVectors",
35-
"integration-node-encrypt": "npm run build; lerna run integration_node --stream --no-prefix -- -- encrypt -m $npm_package_config_encryptManifestList -k $npm_package_config_encryptKeyManifest -o $npm_package_config_decryptOracle",
34+
"integration-node-decrypt": "npm run build; lerna run integration_node --stream --no-prefix -- -- decrypt -v $npm_package_config_localTestVectors -c 10",
35+
"integration-node-encrypt": "npm run build; lerna run integration_node --stream --no-prefix -- -- encrypt -m $npm_package_config_encryptManifestList -k $npm_package_config_encryptKeyManifest -o $npm_package_config_decryptOracle -c 20",
3636
"node-integration": "run-s integration-node-*",
3737
"integration": "run-s integration-*",
3838
"test_conditions": "./util/test_conditions"

0 commit comments

Comments
 (0)