Skip to content

Commit

Permalink
Extract tests into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewosh committed Sep 7, 2021
1 parent bfbcf07 commit a6c1c53
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 206 deletions.
102 changes: 102 additions & 0 deletions test/chaos.js
@@ -0,0 +1,102 @@
const crypto = require('hypercore-crypto')
const random = require('math-random-seed')
const { timeout } = require('nonsynchronous')

const Hyperswarm = require('..')
const { test, destroyAll } = require('./helpers')

const BACKOFFS = [
100,
200,
300
]

test('chaos - recovers after random disconnections (takes ~60s)', async (bootstrap, t) => {
const SEED = 'hyperswarm v3'
const NUM_SWARMS = 10
const NUM_TOPICS = 15
const NUM_FORCE_DISCONNECTS = 30

const STARTUP_DURATION = 1000 * 5
const TEST_DURATION = 1000 * 45
const CHAOS_DURATION = 1000 * 10

const swarms = []
const topics = []
const connections = []
const peersBySwarm = new Map()
const rand = random(SEED)

for (let i = 0; i < NUM_SWARMS; i++) {
const swarm = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 })
swarms.push(swarm)
peersBySwarm.set(swarm, new Set())
swarm.on('connection', conn => {
connections.push(conn)

conn.on('error', noop)
conn.on('close', () => {
clearInterval(timer)
const idx = connections.indexOf(conn)
if (idx === -1) return
connections.splice(idx, 1)
})

const timer = setInterval(() => {
conn.write(Buffer.alloc(10))
}, 100)
conn.write(Buffer.alloc(10))
})
}
for (let i = 0; i < NUM_TOPICS; i++) {
const topic = crypto.randomBytes(32)
topics.push(topic)
}

for (const topic of topics) {
const numSwarms = Math.round(rand() * NUM_SWARMS)
const topicSwarms = []
for (let i = 0; i < numSwarms; i++) {
topicSwarms.push(swarms[Math.floor(rand() * NUM_SWARMS)])
}
for (const swarm of topicSwarms) {
const peers = peersBySwarm.get(swarm)
for (const s of topicSwarms) {
if (swarm === s) continue
peers.add(s.keyPair.publicKey.toString('hex'))
}
await swarm.join(topic).flushed()
}
}

await Promise.all(swarms.map(s => s.flush()))
await timeout(STARTUP_DURATION)

// Randomly destroy connections during the chaos period.
for (let i = 0; i < NUM_FORCE_DISCONNECTS; i++) {
const timeout = Math.floor(rand() * CHAOS_DURATION) // Leave a lot of room at the end for reestablishing connections (timeouts)
setTimeout(() => {
if (!connections.length) return
const idx = Math.floor(rand() * connections.length)
const conn = connections[idx]
conn.destroy()
}, timeout)
}

await timeout(TEST_DURATION) // Wait for the chaos to resolve

for (const [swarm, expectedPeers] of peersBySwarm) {
t.same(swarm.connections.size, expectedPeers.size, 'swarm has the correct number of connections')
const missingKeys = []
for (const conn of swarm.connections) {
const key = conn.remotePublicKey.toString('hex')
if (!expectedPeers.has(key)) missingKeys.push(key)
}
t.same(missingKeys.length, 0, 'swarm is not missing any expected peers')
}

await destroyAll(...swarms)
t.end()
})

function noop () {}
96 changes: 96 additions & 0 deletions test/firewall.js
@@ -0,0 +1,96 @@
const { timeout } = require('nonsynchronous')

const Hyperswarm = require('..')
const { test, destroyAll } = require('./helpers')

const CONNECTION_TIMEOUT = 100
const BACKOFFS = [
100,
200,
300
]

test('firewalled server - bad client is rejected', async (bootstrap, t) => {
const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 })
const swarm2 = new Hyperswarm({
bootstrap,
backoffs: BACKOFFS,
jitter: 0,
firewall: remotePublicKey => {
return !remotePublicKey.equals(swarm1.keyPair.publicKey)
}
})

let serverConnections = 0
swarm2.on('connection', () => serverConnections++)

const topic = Buffer.alloc(32).fill('hello world')
await swarm2.join(topic, { client: false, server: true }).flushed()

swarm1.join(topic, { client: true, server: false })

await timeout(CONNECTION_TIMEOUT)

t.same(serverConnections, 0, 'server did not receive an incoming connection')

await destroyAll(swarm1, swarm2)
t.end()
})

test('firewalled client - bad server is rejected', async (bootstrap, t) => {
const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 })
const swarm2 = new Hyperswarm({
bootstrap,
backoffs: BACKOFFS,
jitter: 0,
firewall: remotePublicKey => {
return !remotePublicKey.equals(swarm1.keyPair.publicKey)
}
})

let clientConnections = 0
swarm2.on('connection', () => clientConnections++)

const topic = Buffer.alloc(32).fill('hello world')
await swarm1.join(topic, { client: false, server: true }).flushed()

swarm2.join(topic, { client: true, server: false })

await timeout(CONNECTION_TIMEOUT)

t.same(clientConnections, 0, 'client did not receive an incoming connection')

await destroyAll(swarm1, swarm2)
t.end()
})

test('firewalled server - rejection does not trigger retry cascade', async (bootstrap, t) => {
const swarm1 = new Hyperswarm({ bootstrap, backoffs: BACKOFFS, jitter: 0 })

let firewallCalls = 0
const swarm2 = new Hyperswarm({
bootstrap,
backoffs: BACKOFFS,
jitter: 0,
firewall: remotePublicKey => {
firewallCalls++
return !remotePublicKey.equals(swarm1.keyPair.publicKey)
}
})

let serverConnections = 0
swarm2.on('connection', () => serverConnections++)

const topic = Buffer.alloc(32).fill('hello world')
await swarm2.join(topic).flushed()

swarm1.join(topic)

await timeout(BACKOFFS[2] * 5) // Wait for many retries -- there should only be 3

t.same(serverConnections, 0, 'server did not receive an incoming connection')
t.same(firewallCalls, 3, 'client retried 3 times')

await destroyAll(swarm1, swarm2)
t.end()
})
64 changes: 63 additions & 1 deletion test/helpers/index.js
Expand Up @@ -2,7 +2,9 @@ const tape = require('tape')
const HyperDHT = require('@hyperswarm/dht')
const Hyperswarm = require('../../')

module.exports = { test, swarm, destroy, defer }
const CONNECTION_TIMEOUT = 100

module.exports = { test, swarm, destroy, destroyAll, timeoutPromise, planPromise, defer }
test.only = (name, fn) => test(name, fn, true, false)
test.skip = (name, fn) => test(name, fn, false, true)

Expand Down Expand Up @@ -64,3 +66,63 @@ async function test (name, fn, only = false, skip = false) {
destroy(nodes)
}
}

async function destroyAll (...swarms) {
for (const swarm of swarms) {
await swarm.clear()
}
for (const swarm of swarms) {
await swarm.destroy()
}
}

function timeoutPromise (ms = CONNECTION_TIMEOUT) {
let res = null
let rej = null
let timer = null

const p = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
p.resolve = res
p.reject = rej
p.reset = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => p.reject(new Error('Timed out')), ms)
}

p.reset()
return p
}

function planPromise (t, count) {
let tick = 0
let res = null
let rej = null
let timer = null

let p = new Promise((resolve, reject) => {
res = resolve
rej = reject
})

return {
plan: (n) => {
const promise = planPromise(t, n)
p = Promise.all([p, promise])
return promise
},
pass: (msg) => {
t.pass(msg)
if (++tick === count) {
if (timer) clearTimeout(timer)
return res()
}
},
timeout: (ms) => {
timer = setTimeout(rej, ms, new Error('Plan promise timed out'))
},
then: (...args) => p.then(...args)
}
}

0 comments on commit a6c1c53

Please sign in to comment.