Skip to content

Commit ae6e463

Browse files
authored
test: conformance testing more modular, no hangs (#266)
* test: conformance testing more modular, no hangs * chore: fix offline helia types * chore: fix lint errors
1 parent cd5a103 commit ae6e463

File tree

10 files changed

+285
-166
lines changed

10 files changed

+285
-166
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"docs:no-publish": "aegir docs --publish false"
3939
},
4040
"devDependencies": {
41-
"aegir": "^47.0.11",
41+
"aegir": "^47.0.21",
4242
"npm-run-all": "^4.1.5"
4343
},
4444
"type": "module",

packages/gateway-conformance/.aegir.js

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,53 +10,11 @@ export default {
1010
},
1111
test: {
1212
files: ['./dist/src/*.spec.js'],
13+
build: false,
1314
before: async (options) => {
1415
if (options.runner !== 'node') {
1516
throw new Error('Only node runner is supported')
1617
}
17-
18-
const { createKuboNode } = await import('./dist/src/fixtures/create-kubo.js')
19-
const KUBO_PORT = await getPort(3440)
20-
const SERVER_PORT = await getPort(3441)
21-
// The Kubo gateway will be passed to the VerifiedFetch config
22-
const { node: controller, gatewayUrl, repoPath } = await createKuboNode(KUBO_PORT)
23-
await controller.start()
24-
const { loadKuboFixtures } = await import('./dist/src/fixtures/kubo-mgmt.js')
25-
const IPFS_NS_MAP = await loadKuboFixtures(repoPath)
26-
const kuboGateway = gatewayUrl
27-
28-
const { startVerifiedFetchGateway } = await import('./dist/src/fixtures/basic-server.js')
29-
const stopBasicServer = await startVerifiedFetchGateway({
30-
serverPort: SERVER_PORT,
31-
kuboGateway,
32-
IPFS_NS_MAP
33-
}).catch((err) => {
34-
log.error(err)
35-
})
36-
37-
const CONFORMANCE_HOST = 'localhost'
38-
39-
return {
40-
controller,
41-
stopBasicServer,
42-
env: {
43-
IPFS_NS_MAP,
44-
CONFORMANCE_HOST,
45-
KUBO_PORT: `${KUBO_PORT}`,
46-
SERVER_PORT: `${SERVER_PORT}`,
47-
KUBO_GATEWAY: kuboGateway
48-
}
49-
}
50-
},
51-
after: async (options, beforeResult) => {
52-
// @ts-expect-error - broken aegir types
53-
await beforeResult.controller.stop()
54-
log('controller stopped')
55-
56-
// @ts-expect-error - broken aegir types
57-
await beforeResult.stopBasicServer()
58-
log('basic server stopped')
59-
6018
}
6119
}
6220
}

packages/gateway-conformance/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,28 @@ $ node dist/src/demo-server.js # in terminal 1
6868
$ curl -v GET http://localhost:3442/ipfs/bafkqabtimvwgy3yk/ # in terminal 2
6969
```
7070

71+
## Example - Generating conformance results standalone
72+
73+
You can generate conformance results independently of the test suite for analysis or reuse:
74+
75+
```console
76+
# Generate results and keep binary for reuse
77+
$ npm run gen:gwc-report
78+
79+
# Generate results with custom name and cleanup binary
80+
$ npm run gen:gwc-report -- my-test --cleanup
81+
82+
# Use the results in your own code
83+
$ node -e "
84+
import { generateConformanceResults } from './dist/src/generate-conformance-report.js';
85+
import { getReportDetails } from './dist/src/get-report-details.js';
86+
const result = await generateConformanceResults('custom', { cleanupBinary: true });
87+
console.log('Report saved to:', result.reportPath);
88+
const details = await getReportDetails(result.reportPath);
89+
console.log('Success rate:', details.successRate);
90+
"
91+
```
92+
7193
## Troubleshooting
7294

7395
### Missing file in gateway-conformance-fixtures folder

packages/gateway-conformance/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@
135135
"dep-check": "aegir dep-check",
136136
"doc-check": "aegir doc-check",
137137
"build": "aegir build",
138-
"test": "aegir test -t node",
138+
"test": "npm run gen:gwc-report && aegir test -t node",
139+
"test:only": "aegir test -t node",
140+
"gen:gwc-report": "npm run build && node dist/src/generate-conformance-report.js",
139141
"update": "npm run build && node dist/src/update-expected-tests.js",
140142
"release": "aegir release"
141143
},
@@ -150,7 +152,7 @@
150152
"@libp2p/logger": "^5.1.17",
151153
"@libp2p/peer-id": "^5.1.4",
152154
"@multiformats/dns": "^1.0.6",
153-
"aegir": "^47.0.11",
155+
"aegir": "^47.0.21",
154156
"blockstore-core": "^5.0.2",
155157
"datastore-core": "^10.0.2",
156158
"execa": "^9.5.3",

packages/gateway-conformance/src/conformance.spec.ts

Lines changed: 7 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,15 @@
11
/* eslint-env mocha */
22
import { access, constants } from 'node:fs/promises'
3-
import { homedir } from 'node:os'
4-
import { join } from 'node:path'
53
import { prefixLogger } from '@libp2p/logger'
64
import { expect } from 'aegir/chai'
7-
import { execa } from 'execa'
8-
import { Agent, setGlobalDispatcher } from 'undici'
9-
import { GWC_IMAGE } from './constants.js'
105
import expectedFailingTests from './expected-failing-tests.json' with { type: 'json' }
116
import expectedPassingTests from './expected-passing-tests.json' with { type: 'json' }
127
import { getReportDetails } from './get-report-details.js'
13-
import { getTestsToRun } from './get-tests-to-run.js'
14-
import { getTestsToSkip } from './get-tests-to-skip.js'
158

169
const logger = prefixLogger('gateway-conformance')
1710

18-
function getGatewayConformanceBinaryPath (): string {
19-
if (process.env.GATEWAY_CONFORMANCE_BINARY != null) {
20-
return process.env.GATEWAY_CONFORMANCE_BINARY
21-
}
22-
const goPath = process.env.GOPATH ?? join(homedir(), 'go')
23-
return join(goPath, 'bin', 'gateway-conformance')
24-
}
25-
26-
function getConformanceTestArgs (name: string, gwcArgs: string[] = [], goTestArgs: string[] = []): string[] {
27-
return [
28-
'test',
29-
`--gateway-url=http://127.0.0.1:${process.env.SERVER_PORT}`,
30-
`--subdomain-url=http://${process.env.CONFORMANCE_HOST}:${process.env.SERVER_PORT}`,
31-
'--verbose',
32-
'--json', `gwc-report-${name}.json`,
33-
...gwcArgs,
34-
'--',
35-
'-timeout', '5m',
36-
...goTestArgs
37-
]
38-
}
39-
4011
describe('@helia/verified-fetch - gateway conformance', function () {
41-
before(async () => {
42-
if (process.env.KUBO_GATEWAY == null) {
43-
throw new Error('KUBO_GATEWAY env var is required')
44-
}
45-
if (process.env.SERVER_PORT == null) {
46-
throw new Error('SERVER_PORT env var is required')
47-
}
48-
if (process.env.CONFORMANCE_HOST == null) {
49-
throw new Error('CONFORMANCE_HOST env var is required')
50-
}
51-
// see https://stackoverflow.com/questions/71074255/use-custom-dns-resolver-for-any-request-in-nodejs
52-
// EVERY undici/fetch request host resolves to local IP. Without this, Node.js does not resolve subdomain requests properly
53-
const staticDnsAgent = new Agent({
54-
connect: {
55-
lookup: (_hostname, _options, callback) => { callback(null, [{ address: '0.0.0.0', family: 4 }]) }
56-
}
57-
})
58-
setGlobalDispatcher(staticDnsAgent)
59-
})
60-
61-
describe('smokeTests', () => {
62-
[
63-
['basic server path request works', `http://localhost:${process.env.SERVER_PORT}/ipfs/bafkqabtimvwgy3yk`],
64-
['basic server subdomain request works', `http://bafkqabtimvwgy3yk.ipfs.localhost:${process.env.SERVER_PORT}`]
65-
].forEach(([name, url]) => {
66-
it(name, async () => {
67-
const resp = await fetch(url)
68-
expect(resp).to.be.ok()
69-
expect(resp.status).to.equal(200)
70-
const text = await resp.text()
71-
expect(text.trim()).to.equal('hello')
72-
})
73-
})
74-
})
75-
7612
describe('conformance testing', () => {
77-
const binaryPath = getGatewayConformanceBinaryPath()
78-
before(async () => {
79-
const log = logger.forComponent('before')
80-
if (process.env.GATEWAY_CONFORMANCE_BINARY != null) {
81-
log('Using custom gateway-conformance binary at %s', binaryPath)
82-
return
83-
}
84-
const gwcVersion = GWC_IMAGE.split(':').pop()
85-
const { stdout, stderr } = await execa('go', ['install', `github.com/ipfs/gateway-conformance/cmd/gateway-conformance@${gwcVersion}`], { reject: true })
86-
log(stdout)
87-
log.error(stderr)
88-
})
89-
90-
after(async () => {
91-
const log = logger.forComponent('after')
92-
93-
if (process.env.GATEWAY_CONFORMANCE_BINARY == null) {
94-
try {
95-
await execa('rm', [binaryPath])
96-
log('gateway-conformance binary successfully uninstalled.')
97-
} catch (error) {
98-
log.error(`Error removing "${binaryPath}"`, error)
99-
}
100-
} else {
101-
log('Not removing custom gateway-conformance binary at %s', binaryPath)
102-
}
103-
})
104-
10513
/**
10614
* You can see what the latest success rate, passing tests, and failing tests are by running the following command:
10715
*
@@ -110,34 +18,26 @@ describe('@helia/verified-fetch - gateway conformance', function () {
11018
* ```
11119
*/
11220
describe('gateway conformance', function () {
113-
this.timeout(200000)
11421
let successRate: number
11522
let failingTests: string[]
11623
let passingTests: string[]
11724
const log = logger.forComponent('output:all')
11825

11926
before(async function () {
120-
const testsToSkip: string[] = getTestsToSkip()
121-
const testsToRun: string[] = getTestsToRun()
122-
const cancelSignal = AbortSignal.timeout(200000)
123-
const { stderr, stdout } = await execa(binaryPath, getConformanceTestArgs('all', [], [
124-
...(testsToRun.length > 0 ? ['-run', `${testsToRun.join('|')}`] : []),
125-
...(testsToSkip.length > 0 ? ['-skip', `${testsToSkip.join('|')}`] : [])
126-
]), { reject: false, cancelSignal })
127-
128-
expect(cancelSignal.aborted).to.be.false()
129-
130-
log(stdout)
131-
log.error(stderr)
13227
await expect(access('gwc-report-all.json', constants.R_OK)).to.eventually.be.fulfilled()
13328
const results = await getReportDetails('gwc-report-all.json')
13429
successRate = results.successRate
13530
failingTests = results.failingTests
13631
passingTests = results.passingTests
32+
13733
log.trace('Passing tests:')
138-
passingTests.forEach((test) => { log.trace(`PASS: ${test}`) })
34+
for (const test of passingTests) {
35+
log.trace(`PASS: ${test}`)
36+
}
13937
log.trace('Failing tests:')
140-
failingTests.forEach((test) => { log.trace(`FAIL: ${test}`) })
38+
for (const test of failingTests) {
39+
log.trace(`FAIL: ${test}`)
40+
}
14141
})
14242

14343
for (const test of expectedPassingTests) {

packages/gateway-conformance/src/fixtures/basic-server.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { dagCborHtmlPreviewPluginFactory, dirIndexHtmlPluginFactory } from '@hel
66
import { logger } from '@libp2p/logger'
77
import { dns } from '@multiformats/dns'
88
import { MemoryBlockstore } from 'blockstore-core'
9-
import { Agent, setGlobalDispatcher } from 'undici'
109
import { createVerifiedFetch } from './create-verified-fetch.js'
1110
import { getLocalDnsResolver } from './get-local-dns-resolver.js'
1211
import { convertFetchHeadersToNodeJsHeaders, convertNodeJsHeadersToFetchHeaders } from './header-utils.js'
@@ -174,12 +173,6 @@ async function callVerifiedFetch (req: IncomingMessage, res: Response, { serverP
174173
}
175174

176175
export async function startVerifiedFetchGateway ({ kuboGateway, serverPort, IPFS_NS_MAP }: BasicServerOptions): Promise<() => Promise<void>> {
177-
const staticDnsAgent = new Agent({
178-
connect: {
179-
lookup: (_hostname, _options, callback) => { callback(null, [{ address: '0.0.0.0', family: 4 }]) }
180-
}
181-
})
182-
setGlobalDispatcher(staticDnsAgent)
183176
kuboGateway = kuboGateway ?? process.env.KUBO_GATEWAY
184177
const useSessions = process.env.USE_SESSIONS !== 'false'
185178

@@ -224,16 +217,18 @@ export async function startVerifiedFetchGateway ({ kuboGateway, serverPort, IPFS
224217
console.log(`Basic server listening on port ${serverPort}`)
225218
})
226219

227-
return async () => {
228-
log('Stopping...')
220+
return async function cleanup () {
221+
log('Stopping basic server...')
229222
await new Promise<void>((resolve, reject) => {
230223
// no matter what happens, we need to kill the server
231224
server.closeAllConnections()
232225
log('Closed all connections')
233226
server.close((err: any) => {
234227
if (err != null) {
228+
log.error('Error closing server - %e', err)
235229
reject(err instanceof Error ? err : new Error(err))
236230
} else {
231+
log('Server closed successfully')
237232
resolve()
238233
}
239234
})

0 commit comments

Comments
 (0)