Skip to content

Commit 3ac7a34

Browse files
authored
Merge pull request #796 from axiomatic-aardvark/master
Add Docker support for unit testing with Matchstick
2 parents c8a5cab + 79f3f13 commit 3ac7a34

File tree

1 file changed

+210
-27
lines changed

1 file changed

+210
-27
lines changed

src/commands/test.js

Lines changed: 210 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,134 @@ const { Binary } = require('binary-install-raw')
22
const os = require('os')
33
const chalk = require('chalk')
44
const fetch = require('node-fetch')
5+
const { filesystem, print } = require('gluegun')
6+
const { fixParameters } = require('../command-helpers/gluegun')
57
const semver = require('semver')
8+
const { spawn, exec } = require('child_process')
9+
const yaml = require('js-yaml')
610

711
const HELP = `
812
${chalk.bold('graph test')} ${chalk.dim('[options]')} ${chalk.bold('<datasource>')}
913
1014
${chalk.dim('Options:')}
11-
12-
-f --force Overwrite folder + file when downloading
15+
-c, --coverage Run the tests in coverage mode. Works with v0.2.1 and above
16+
-d, --docker Run the tests in a docker container(Note: Please execute from the root folder of the subgraph)
17+
-f --force Binary - overwrites folder + file when downloading. Docker - rebuilds the docker image
1318
-h, --help Show usage information
1419
-l, --logs Logs to the console information about the OS, CPU model and download url (debugging purposes)
20+
-r, --recompile Force-recompile tests (Not available in 0.2.2 and earlier versions)
1521
-v, --version <tag> Choose the version of the rust binary that you want to be downloaded/used
1622
`
1723

1824
module.exports = {
1925
description: 'Runs rust binary for subgraph testing',
2026
run: async toolbox => {
21-
// Obtain tools
22-
let { print } = toolbox
23-
2427
// Read CLI parameters
25-
let { f, force, h, help, l, logs, v, version } = toolbox.parameters.options
26-
let datasource = toolbox.parameters.first
28+
let {
29+
c,
30+
coverage,
31+
d,
32+
docker,
33+
f,
34+
force,
35+
h,
36+
help,
37+
l,
38+
logs,
39+
r,
40+
recompile,
41+
v,
42+
version,
43+
} = toolbox.parameters.options
2744

45+
let opts = new Map()
2846
// Support both long and short option variants
29-
force = force || f
30-
help = help || h
31-
logs = logs || l
32-
version = version || v
47+
opts.set("coverage", coverage || c)
48+
opts.set("docker", docker || d)
49+
opts.set("force", force || f)
50+
opts.set("help", help || h)
51+
opts.set("logs", logs || l)
52+
opts.set("recompile", recompile || r)
53+
opts.set("version", version || v)
54+
55+
// Fix if a boolean flag (e.g -c, --coverage) has an argument
56+
try {
57+
fixParameters(toolbox.parameters, {
58+
h,
59+
help,
60+
c,
61+
coverage,
62+
d,
63+
docker,
64+
f,
65+
force,
66+
l,
67+
logs,
68+
r,
69+
recompile
70+
})
71+
} catch (e) {
72+
print.error(e.message)
73+
process.exitCode = 1
74+
return
75+
}
76+
77+
let datasource = toolbox.parameters.first || toolbox.parameters.array[0]
3378

3479
// Show help text if requested
35-
if (help) {
80+
if (opts.get("help")) {
3681
print.info(HELP)
3782
return
3883
}
3984

40-
const platform = getPlatform(logs)
41-
if (!version) {
42-
let result = await fetch('https://api.github.com/repos/LimeChain/matchstick/releases/latest')
43-
let json = await result.json()
44-
version = json.tag_name
85+
let result = await fetch('https://api.github.com/repos/LimeChain/matchstick/releases/latest')
86+
let json = await result.json()
87+
opts.set("latestVersion", json.tag_name)
88+
89+
if(opts.get("docker")) {
90+
runDocker(datasource, opts)
91+
} else {
92+
runBinary(datasource, opts)
4593
}
94+
}
95+
}
4696

47-
const url = `https://github.com/LimeChain/matchstick/releases/download/${version}/${platform}`
97+
async function runBinary(datasource, opts) {
98+
let coverageOpt = opts.get("coverage")
99+
let forceOpt = opts.get("force")
100+
let logsOpt = opts.get("logs")
101+
let versionOpt = opts.get("version")
102+
let latestVersion = opts.get("latestVersion")
103+
let recompileOpt = opts.get("recompile")
48104

49-
if (logs) {
50-
console.log(`Download link: ${url}`)
51-
}
105+
const platform = getPlatform(logsOpt)
52106

53-
let binary = new Binary(platform, url, version)
54-
await binary.install(force)
55-
datasource ? binary.run(datasource) : binary.run()
107+
const url = `https://github.com/LimeChain/matchstick/releases/download/${versionOpt || latestVersion}/${platform}`
108+
109+
if (logsOpt) {
110+
print.info(`Download link: ${url}`)
56111
}
112+
113+
let binary = new Binary(platform, url, versionOpt || latestVersion)
114+
forceOpt ? await binary.install(true) : await binary.install(false)
115+
let args = new Array()
116+
117+
if (coverageOpt) args.push('-c')
118+
if (recompileOpt) args.push('-r')
119+
if (datasource) args.push(datasource)
120+
args.length > 0 ? binary.run(...args) : binary.run()
57121
}
58122

59-
function getPlatform(logs) {
123+
function getPlatform(logsOpt) {
60124
const type = os.type()
61125
const arch = os.arch()
62126
const release = os.release()
63127
const cpuCore = os.cpus()[0]
64128
const majorVersion = semver.major(release)
65129
const isM1 = cpuCore.model.includes("Apple M1")
66130

67-
if (logs) {
68-
console.log(`OS type: ${type}\nOS arch: ${arch}\nOS release: ${release}\nOS major version: ${majorVersion}\nCPU model: ${cpuCore.model}`)
131+
if (logsOpt) {
132+
print.info(`OS type: ${type}\nOS arch: ${arch}\nOS release: ${release}\nOS major version: ${majorVersion}\nCPU model: ${cpuCore.model}`)
69133
}
70134

71135
if (arch === 'x64' || (arch === 'arm64' && isM1)) {
@@ -90,3 +154,122 @@ function getPlatform(logs) {
90154

91155
throw new Error(`Unsupported platform: ${type} ${arch} ${majorVersion}`)
92156
}
157+
158+
async function runDocker(datasource, opts) {
159+
let coverageOpt = opts.get("coverage")
160+
let forceOpt = opts.get("force")
161+
let versionOpt = opts.get("version")
162+
let latestVersion = opts.get("latestVersion")
163+
let recompileOpt = opts.get("recompile")
164+
165+
// Remove binary-install-raw binaries, because docker has permission issues
166+
// when building the docker images
167+
await filesystem.remove("./node_modules/binary-install-raw/bin")
168+
169+
// Get current working directory
170+
let current_folder = await filesystem.cwd()
171+
172+
// Build the Dockerfile location. Defaults to ./tests/.docker if
173+
// a custom testsFolder is not declared in the subgraph.yaml
174+
let dockerDir = ""
175+
176+
try {
177+
let doc = await yaml.load(filesystem.read('subgraph.yaml', 'utf8'))
178+
testsFolder = doc.testsFolder || './tests'
179+
dockerDir = testsFolder.endsWith('/') ? testsFolder + '.docker' : testsFolder + '/.docker'
180+
} catch (error) {
181+
print.error(error.message)
182+
return
183+
}
184+
185+
// Create the Dockerfile
186+
try {
187+
await filesystem.write(`${dockerDir}/Dockerfile`, dockerfile(versionOpt, latestVersion))
188+
print.info('Successfully generated Dockerfile.')
189+
} catch (error) {
190+
print.info('A problem occurred while generating the Dockerfile. Please attend to the errors below:')
191+
print.error(error.message)
192+
return
193+
}
194+
195+
// Run a command to check if matchstick image already exists
196+
exec('docker images -q matchstick', (error, stdout, stderr) => {
197+
// Collect all(if any) flags and options that have to be passed to the matchstick binary
198+
let testArgs = ''
199+
if (coverageOpt) testArgs = testArgs + ' -c'
200+
if (recompileOpt) testArgs = testArgs + ' -r'
201+
if (datasource) testArgs = testArgs + ' ' + datasource
202+
203+
// Build the `docker run` command options and flags
204+
let dockerRunOpts = ['run', '-it', '--rm', '--mount', `type=bind,source=${current_folder},target=/matchstick`]
205+
206+
if(testArgs !== '') {
207+
dockerRunOpts.push('-e')
208+
dockerRunOpts.push(`ARGS=${testArgs.trim()}`)
209+
}
210+
211+
dockerRunOpts.push('matchstick')
212+
213+
// If a matchstick image does not exists, the command returns an empty string,
214+
// else it'll return the image ID. Skip `docker build` if an image already exists
215+
// If `-v/--version` is specified, delete current image(if any) and rebuild.
216+
// Use spawn() and {stdio: 'inherit'} so we can see the logs in real time.
217+
if(stdout === '' || versionOpt || forceOpt) {
218+
if ((stdout !== '' && versionOpt) || forceOpt) {
219+
exec('docker image rm matchstick', (error, stdout, stderr) => {
220+
print.info(chalk.bold(`Removing matchstick image\n${stdout}`))
221+
})
222+
}
223+
// Build a docker image. If the process has executed successfully
224+
// run a container from that image.
225+
spawn(
226+
'docker',
227+
['build', '--no-cache', '-f', `${dockerDir}/Dockerfile`, '-t', 'matchstick', '.'],
228+
{ stdio: 'inherit' }
229+
).on('close', code => {
230+
if (code === 0) {
231+
spawn('docker', dockerRunOpts, { stdio: 'inherit' })
232+
}
233+
})
234+
} else {
235+
print.info("Docker image already exists. Skipping `docker build` command.")
236+
// Run the container from the existing matchstick docker image
237+
spawn('docker', dockerRunOpts, { stdio: 'inherit' })
238+
}
239+
})
240+
}
241+
242+
// TODO: Move these in separate file (in a function maybe)
243+
function dockerfile(versionOpt, latestVersion) {
244+
return `
245+
FROM ubuntu:20.04
246+
ENV ARGS=""
247+
248+
# Install necessary packages
249+
RUN apt update
250+
RUN apt install -y nodejs
251+
RUN apt install -y npm
252+
RUN apt install -y git
253+
RUN apt install -y postgresql
254+
RUN apt install -y curl
255+
RUN apt install -y cmake
256+
RUN npm install -g @graphprotocol/graph-cli
257+
258+
# Download the latest linux binary
259+
RUN curl -OL https://github.com/LimeChain/matchstick/releases/download/${versionOpt || latestVersion}/binary-linux-20
260+
261+
# Make it executable
262+
RUN chmod a+x binary-linux-20
263+
264+
# Create a matchstick dir where the host will be copied
265+
RUN mkdir matchstick
266+
WORKDIR matchstick
267+
268+
# Copy host to /matchstick
269+
COPY ../ .
270+
271+
RUN graph codegen
272+
RUN graph build
273+
274+
CMD ../binary-linux-20 \${ARGS}`
275+
}

0 commit comments

Comments
 (0)