Skip to content

Commit 4f42860

Browse files
authored
feat: test dependants (#525)
Generalises the `test-external` command to replace arbitrary modules in third party repos and run their tests. Run it like this: ```console $ aegir test-dependant git@github.com:org/project.git --deps=foo@1.0.0,bar@2.0.0 ``` `git@github.com:org/project.git` will be cloned to a temporary folder, dependencies installed and tests run. Then the dependencies specified in the `--deps` arg will be upgraded and the tests run again. `--deps` should contain the project you want to test, and can be anything npm can resolve as a version number, for example github URLs: `--deps=foo@github:org/project.git#$TRAVIS_COMMIT` Lerna based monorepos are supported.
1 parent 385ecb8 commit 4f42860

File tree

11 files changed

+248
-71
lines changed

11 files changed

+248
-71
lines changed

.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
include:
2121
- os: windows
2222
cache: false
23-
23+
2424
- stage: check
2525
script:
2626
- node cli.js commitlint --travis

cmds/test-dependant.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
module.exports = {
4+
command: 'test-dependant [repo]',
5+
desc: 'Run the tests of an module that depends on this module to see if the current changes have caused a regression',
6+
builder: {
7+
repo: {
8+
describe: 'The dependant module\'s repo URL',
9+
type: 'string'
10+
},
11+
branch: {
12+
describe: 'A branch to use from the dependant repo',
13+
type: 'string'
14+
},
15+
moduleName: {
16+
describe: 'A branch to use from the dependant repo',
17+
type: 'string'
18+
},
19+
deps: {
20+
describe: 'Other dependencies to override, e.g. --deps=foo@1.5.0,bar@2.4.1',
21+
coerce: (val) => {
22+
if (typeof val !== 'string') {
23+
return {}
24+
}
25+
26+
const deps = {}
27+
28+
for (const dep of val.split(',')) {
29+
const parts = dep.split('@')
30+
deps[parts[0]] = parts[1]
31+
}
32+
33+
return deps
34+
},
35+
default: {}
36+
}
37+
},
38+
handler (argv) {
39+
const cmd = require('../src/test-dependant')
40+
const onError = require('../src/error-handler')
41+
cmd(argv).catch(onError)
42+
}
43+
}

cmds/test-external.js

-25
This file was deleted.

src/test-external/index.js renamed to src/test-dependant/index.js

+51-45
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@ const {
88
const fs = require('fs-extra')
99
const glob = require('it-glob')
1010

11-
const findHttpClientPkg = (targetDir) => {
12-
const location = require.resolve('ipfs-http-client', {
13-
paths: [
14-
targetDir
15-
]
16-
})
17-
18-
return require(location.replace('src/index.js', 'package.json'))
19-
}
20-
2111
const isDep = (name, pkg) => {
2212
return Object.keys(pkg.dependencies || {}).filter(dep => dep === name).pop()
2313
}
@@ -47,6 +37,16 @@ const hasYarnLock = (targetDir) => {
4737
return fs.existsSync(path.join(targetDir, 'yarn.lock'))
4838
}
4939

40+
const printFailureMessage = (message) => {
41+
console.info('-------------------------------------------------------------------') // eslint-disable-line no-console
42+
console.info('') // eslint-disable-line no-console
43+
console.info(message) // eslint-disable-line no-console
44+
console.info('') // eslint-disable-line no-console
45+
console.info('Dependant project has not been tested with updated dependencies') // eslint-disable-line no-console
46+
console.info('') // eslint-disable-line no-console
47+
console.info('-------------------------------------------------------------------') // eslint-disable-line no-console
48+
}
49+
5050
const installDependencies = async (targetDir) => {
5151
console.info('Installing dependencies') // eslint-disable-line no-console
5252
if (hasYarnLock(targetDir)) {
@@ -64,46 +64,56 @@ const installDependencies = async (targetDir) => {
6464
}
6565
}
6666

67-
const linkIPFSInDir = async (targetDir, ipfsDir, ipfsPkg, httpClientPkg) => {
67+
const upgradeDependenciesInDir = async (targetDir, deps) => {
6868
const modulePkgPath = path.join(targetDir, 'package.json')
6969
const modulePkg = require(modulePkgPath)
7070

71-
// if the project also depends on the http client, upgrade it to the same version we are using
72-
if (dependsOn('ipfs-http-client', modulePkg)) {
73-
console.info('Upgrading ipfs-http-client dependency to', httpClientPkg.version) // eslint-disable-line no-console
74-
await exec('npm', ['install', `ipfs-http-client@${httpClientPkg.version}`], {
75-
cwd: targetDir
76-
})
71+
modulePkg.dependencies = modulePkg.dependencies || {}
72+
modulePkg.peerDependencies = modulePkg.peerDependencies || {}
73+
modulePkg.optionalDependencies = modulePkg.optionalDependencies || {}
74+
modulePkg.devDependencies = modulePkg.devDependencies || {}
75+
76+
console.info('Upgrading deps') // eslint-disable-line no-console
77+
78+
for (const dep of Object.keys(deps)) {
79+
const existingVersion = modulePkg.dependencies[dep] || modulePkg.peerDependencies[dep] || modulePkg.optionalDependencies[dep] || modulePkg.devDependencies[dep]
80+
console.info('Upgrading', dep, 'from version', existingVersion, 'to', deps[dep]) // eslint-disable-line no-console
81+
82+
if (modulePkg.dependencies[dep] || modulePkg.peerDependencies[dep] || modulePkg.optionalDependencies[dep]) {
83+
modulePkg.dependencies[dep] = deps[dep]
84+
} else if (modulePkg.devDependencies[dep]) {
85+
modulePkg.devDependencies[dep] = deps[dep]
86+
}
7787
}
7888

79-
console.info(`Linking ipfs@${ipfsPkg.version} in dir ${targetDir}`) // eslint-disable-line no-console
80-
await exec('npx', ['connect-deps', 'link', path.relative(await fs.realpath(targetDir), await fs.realpath(ipfsDir))], {
81-
cwd: targetDir
82-
})
83-
await exec('npx', ['connect-deps', 'connect'], {
89+
await fs.writeFile(modulePkgPath, JSON.stringify(modulePkg, null, 2))
90+
91+
await exec('npm', ['install'], {
8492
cwd: targetDir
8593
})
8694
}
8795

88-
const testModule = async (targetDir, ipfsDir, ipfsPkg, httpClientPkg) => {
96+
const testModule = async (targetDir, deps) => {
8997
const pkgPath = path.join(targetDir, 'package.json')
9098

9199
if (!fs.existsSync(pkgPath)) {
92-
console.info(`No package.json found at ${pkgPath}`) // eslint-disable-line no-console
100+
printFailureMessage(`No package.json found at ${pkgPath}`)
93101

94102
return
95103
}
96104

97105
const modulePkg = require(pkgPath)
98106

99-
if (!dependsOn('ipfs', modulePkg) && !dependsOn('ipfs-http-client', modulePkg)) {
100-
console.info(`Module ${modulePkg.name} does not depend on IPFS or the IPFS HTTP Client`) // eslint-disable-line no-console
107+
for (const dep of Object.keys(deps)) {
108+
if (!dependsOn(dep, modulePkg)) {
109+
printFailureMessage(`Module ${modulePkg.name} does not depend on ${dep}`)
101110

102-
return
111+
return
112+
}
103113
}
104114

105115
if (!modulePkg.scripts || !modulePkg.scripts.test) {
106-
console.info(`Module ${modulePkg.name} has no tests`) // eslint-disable-line no-console
116+
printFailureMessage(`Module ${modulePkg.name} has no tests`)
107117

108118
return
109119
}
@@ -114,26 +124,26 @@ const testModule = async (targetDir, ipfsDir, ipfsPkg, httpClientPkg) => {
114124
cwd: targetDir
115125
})
116126
} catch (err) {
117-
console.info(`Failed to run the tests of ${modulePkg.name}, aborting`, err.message) // eslint-disable-line no-console
127+
printFailureMessage(`Failed to run the tests of ${modulePkg.name}, aborting`, err.message)
118128

119129
return
120130
}
121131

122-
// upgrade IPFS to the rc
123-
await linkIPFSInDir(targetDir, ipfsDir, ipfsPkg, httpClientPkg)
132+
// upgrade passed dependencies
133+
await upgradeDependenciesInDir(targetDir, deps)
124134

125-
// run the tests with the new IPFS/IPFSHTTPClient
135+
// run the tests with the new deps
126136
await exec('npm', ['test'], {
127137
cwd: targetDir
128138
})
129139
}
130140

131-
const testRepo = async (targetDir, ipfsDir, ipfsPkg, httpClientPkg) => {
141+
const testRepo = async (targetDir, deps) => {
132142
await installDependencies(targetDir)
133-
await testModule(targetDir, ipfsDir, ipfsPkg, httpClientPkg)
143+
await testModule(targetDir, deps)
134144
}
135145

136-
const testMonoRepo = async (targetDir, ipfsDir, ipfsPkg, httpClientPkg) => {
146+
const testMonoRepo = async (targetDir, deps) => {
137147
await installDependencies(targetDir)
138148

139149
let lerna = path.join('node_modules', '.bin', 'lerna')
@@ -163,17 +173,13 @@ const testMonoRepo = async (targetDir, ipfsDir, ipfsPkg, httpClientPkg) => {
163173
// test each package that depends on ipfs/http client
164174
for (const pattern of packages) {
165175
for await (const match of glob(targetDir, pattern)) {
166-
await testModule(path.join(targetDir, match), ipfsDir, ipfsPkg, httpClientPkg)
176+
await testModule(path.join(targetDir, match), deps)
167177
}
168178
}
169179
}
170180

171-
async function testExternal (opts) {
172-
// work out our versions
173-
const ipfsDir = process.cwd()
174-
const ipfsPkg = require(path.join(ipfsDir, 'package.json'))
175-
const httpClientPkg = findHttpClientPkg(ipfsDir)
176-
const targetDir = path.join(os.tmpdir(), `${opts.name}-${Date.now()}`)
181+
async function testDependant (opts) {
182+
const targetDir = path.join(os.tmpdir(), `test-dependant-${Date.now()}`)
177183

178184
console.info(`Cloning ${opts.repo} into ${targetDir}`) // eslint-disable-line no-console
179185
await exec('git', ['clone', opts.repo, targetDir], {
@@ -187,13 +193,13 @@ async function testExternal (opts) {
187193
}
188194

189195
if (isMonoRepo(targetDir)) {
190-
await testMonoRepo(targetDir, ipfsDir, ipfsPkg, httpClientPkg)
196+
await testMonoRepo(targetDir, opts.deps)
191197
} else {
192-
await testRepo(targetDir, ipfsDir, ipfsPkg, httpClientPkg)
198+
await testRepo(targetDir, opts.deps)
193199
}
194200

195201
console.info(`Removing ${targetDir}`) // eslint-disable-line no-console
196202
await fs.remove(targetDir)
197203
}
198204

199-
module.exports = testExternal
205+
module.exports = testDependant

test/dependants.spec.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const expect = require('chai').expect
5+
const execa = require('execa')
6+
const fs = require('fs-extra')
7+
const path = require('path')
8+
const bin = require.resolve('../')
9+
const os = require('os')
10+
11+
const dirs = {}
12+
13+
const git = {
14+
init: async (name) => {
15+
const source = path.join(__dirname, 'fixtures', 'test-dependant', name)
16+
dirs[name] = path.join(os.tmpdir(), `repo-${Math.random()}`)
17+
18+
await fs.mkdir(dirs[name])
19+
await fs.copy(source, dirs[name])
20+
21+
await execa('git', ['init'], {
22+
cwd: dirs[name]
23+
})
24+
await execa('git', ['config', 'user.email', 'test@test.com'], {
25+
cwd: dirs[name]
26+
})
27+
await execa('git', ['config', 'user.name', 'test'], {
28+
cwd: dirs[name]
29+
})
30+
await git.add(name)
31+
await git.commit(name, 'initial commit')
32+
},
33+
add: async (name) => {
34+
await execa('git', ['add', '-A'], {
35+
cwd: dirs[name]
36+
})
37+
},
38+
commit: async (name, message) => {
39+
await execa('git', ['commit', '-m', message], {
40+
cwd: dirs[name]
41+
})
42+
}
43+
}
44+
45+
describe('dependants', function () {
46+
this.timeout(120000)
47+
48+
if (os.platform() === 'win32') {
49+
// TODO: travis windows builds can't clone git repos from the local
50+
// filesystem as it errors with "git-upload-pack: command not found"
51+
return
52+
}
53+
54+
afterEach(async () => {
55+
await Promise.all(
56+
Object.keys(dirs).map(dir => fs.remove(dirs[dir]))
57+
)
58+
})
59+
60+
describe('regular project', () => {
61+
beforeEach(async () => {
62+
await git.init('project')
63+
})
64+
65+
it('should test a regular project', async () => {
66+
const diff = `derp-${Math.random()}`
67+
const output = await execa(bin, ['test-dependant', dirs.project, '--deps=it-all@1.0.1'], {
68+
env: {
69+
DISAMBIGUATOR: diff
70+
}
71+
})
72+
73+
expect(output.stdout).to.include(`${diff}-dependency-version=1.0.0`)
74+
expect(output.stdout).to.include(`${diff}-dependency-version=1.0.1`)
75+
})
76+
})
77+
78+
describe('monorepo', () => {
79+
beforeEach(async () => {
80+
await git.init('monorepo')
81+
})
82+
83+
it('should test a monorepo', async () => {
84+
const diff = `derp-${Math.random()}`
85+
const output = await execa(bin, ['test-dependant', dirs.monorepo, '--deps=it-all@1.0.1'], {
86+
env: {
87+
DISAMBIGUATOR: diff
88+
}
89+
})
90+
91+
expect(output.stdout).to.include(`${diff}-dependency-version=1.0.0`)
92+
expect(output.stdout).to.include(`${diff}-dependency-version=1.0.1`)
93+
})
94+
})
95+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"lerna": "3.20.2",
3+
"packages": [
4+
"packages/*"
5+
],
6+
"version": "independent",
7+
"command": {
8+
"bootstrap": {
9+
"hoist": true
10+
},
11+
"run": {
12+
"stream": true
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "monorepo",
3+
"version": "1.0.0",
4+
"devDependencies": {
5+
"lerna": "^3.20.2"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "submodule",
3+
"version": "1.0.0",
4+
"description": "",
5+
"scripts": {
6+
"test": "node ./test.js"
7+
},
8+
"author": "",
9+
"license": "ISC",
10+
"dependencies": {
11+
"it-all": "1.0.0"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict'
2+
3+
const pkg = require('it-all/package.json')
4+
5+
console.info(`${process.env.DISAMBIGUATOR}-dependency-version=${pkg.version}`) // eslint-disable-line no-console

0 commit comments

Comments
 (0)