Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
68
gaia/tests/jsmarionette/runner/mozilla-runner/examples/firefox.js
Executable file
68
gaia/tests/jsmarionette/runner/mozilla-runner/examples/firefox.js
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
/**
|
||||
* This is a demo of using multiple mozilla-* projects with mozilla-runner.
|
||||
*
|
||||
* - downloads firefox
|
||||
* - creates a profile
|
||||
* - launches firefox (with a custom profile) to a given url
|
||||
* - outputs the "dump'ed" content to stdout
|
||||
*/
|
||||
var mozdown = require('mozilla-download'),
|
||||
mozprofile = require('mozilla-profile-builder'),
|
||||
mozrun = require('../index'),
|
||||
Static = require('node-static'),
|
||||
fs = require('fs');
|
||||
|
||||
var PORT = 60034;
|
||||
var FIREFOX_PATH = __dirname + '/firefox';
|
||||
var PREFS = {
|
||||
'browser.dom.window.dump.enabled': true
|
||||
};
|
||||
|
||||
function download(callback) {
|
||||
console.log('---*(-)- Downloading Firefox -----');
|
||||
// download firefox if its missing
|
||||
mozdown.download('firefox', FIREFOX_PATH, function(err) {
|
||||
if (err) return console.error(err);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
// start static server
|
||||
var file = new(Static.Server)(__dirname + '/public');
|
||||
require('http').createServer(function(request, response) {
|
||||
request.addListener('end', function() {
|
||||
file.serve(request, response);
|
||||
}).resume();
|
||||
}).listen(PORT);
|
||||
}
|
||||
|
||||
function profile(product, path, options, callback) {
|
||||
mozprofile.firefox.profile({ userPrefs: PREFS }, function(err, path) {
|
||||
callback(err, path);
|
||||
});
|
||||
}
|
||||
|
||||
function demo() {
|
||||
// invoke the server
|
||||
startServer();
|
||||
|
||||
var options = {
|
||||
profile: profile,
|
||||
url: 'http://localhost:' + PORT + '/index.html'
|
||||
};
|
||||
|
||||
mozrun.run('firefox', FIREFOX_PATH, options, function(err, child) {
|
||||
child.stdout.on('data', function(content) {
|
||||
console.log('[firefox] %s', content.toString());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!fs.existsSync(FIREFOX_PATH)) {
|
||||
download(demo);
|
||||
} else {
|
||||
demo();
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"iamsocool": true
|
||||
}
|
||||
19
gaia/tests/jsmarionette/runner/mozilla-runner/examples/public/index.html
Executable file
19
gaia/tests/jsmarionette/runner/mozilla-runner/examples/public/index.html
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<title>Demo target</title>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', './content.json', true);
|
||||
xhr.onload = function() {
|
||||
dump(xhr.responseText);
|
||||
};
|
||||
xhr.send();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
DEMO YEY!
|
||||
</body>
|
||||
</html>
|
||||
4
gaia/tests/jsmarionette/runner/mozilla-runner/index.js
Executable file
4
gaia/tests/jsmarionette/runner/mozilla-runner/index.js
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
run: require('./lib/run').run,
|
||||
detectBinary: require('./lib/detectbinary').detectBinary
|
||||
};
|
||||
67
gaia/tests/jsmarionette/runner/mozilla-runner/lib/detectbinary.js
Executable file
67
gaia/tests/jsmarionette/runner/mozilla-runner/lib/detectbinary.js
Executable file
|
|
@ -0,0 +1,67 @@
|
|||
'use strict';
|
||||
var fs = require('fs'),
|
||||
fsPath = require('path');
|
||||
|
||||
module.exports.defaultBin = 'firefox-bin';
|
||||
|
||||
var platformPaths = {
|
||||
darwin: 'Contents/MacOS'
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to locate binary based on runtime/platform.
|
||||
*
|
||||
* detectBinary(
|
||||
* '/Applications/Firefox',
|
||||
* { product: 'firefox' },
|
||||
* function(err, firefox) {
|
||||
* // firefox === '/Applications/Firefox/Contents/MacOS/firefox-bin'
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* Options:
|
||||
* - bin: "firefox-bin" by default
|
||||
* - platform: defaults to process.platform
|
||||
*
|
||||
* @param {String} source of product runtime.
|
||||
* @param {Object} options for detection.
|
||||
* @param {Function} callback [err, binaryPath];
|
||||
*/
|
||||
function detectBinary(source, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!options.product) throw new Error('.product must be given');
|
||||
|
||||
var platform = options.platform || process.platform;
|
||||
var dir = platformPaths[platform] || '';
|
||||
|
||||
var bins = [
|
||||
fsPath.join(source, dir, options.product + '-bin'),
|
||||
fsPath.join(source, dir, options.product)
|
||||
];
|
||||
|
||||
function exists(bin) {
|
||||
fs.exists(bin, function(itDoes) {
|
||||
if (itDoes) return callback(null, bin);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function next() {
|
||||
var bin = bins.shift();
|
||||
if (!bin) {
|
||||
return callback(
|
||||
new Error('cannot find "' + options.product + '" binary in ' + source)
|
||||
);
|
||||
}
|
||||
|
||||
exists(bin);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports.detectBinary = detectBinary;
|
||||
132
gaia/tests/jsmarionette/runner/mozilla-runner/lib/run.js
Executable file
132
gaia/tests/jsmarionette/runner/mozilla-runner/lib/run.js
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
'use strict';
|
||||
var detectBinary = require('./detectbinary').detectBinary,
|
||||
debug = require('debug')('mozilla-runner:run'),
|
||||
spawn = require('child_process').spawn,
|
||||
fs = require('fs');
|
||||
|
||||
// private helper for building the argv
|
||||
function buildArgv(options) {
|
||||
var argv = [];
|
||||
|
||||
if (options.argv)
|
||||
argv = argv.concat(options.argv);
|
||||
|
||||
if (options.profile) {
|
||||
argv.push('-profile');
|
||||
argv.push(options.profile);
|
||||
}
|
||||
|
||||
if (options.screen &&
|
||||
options.screen.width &&
|
||||
options.screen.height) {
|
||||
|
||||
argv.push('--screen=' +
|
||||
options.screen.width + 'x' +
|
||||
options.screen.height + (
|
||||
options.screen.dpi ?
|
||||
('@' + options.screen.dpi) :
|
||||
''));
|
||||
}
|
||||
|
||||
if (options.noRemote !== false) {
|
||||
argv.push('-no-remote');
|
||||
}
|
||||
|
||||
if (options.url) {
|
||||
argv.push(options.url);
|
||||
}
|
||||
|
||||
if (options.oop) {
|
||||
argv.push('-oop');
|
||||
}
|
||||
|
||||
if (options.chrome) {
|
||||
argv.push('-chrome');
|
||||
argv.push(options.chrome);
|
||||
}
|
||||
|
||||
if (options.startDebugger) {
|
||||
argv.push('-start-debugger-server');
|
||||
argv.push(options.startDebugger);
|
||||
}
|
||||
|
||||
return argv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an instance of a mozilla runtime/product.
|
||||
*
|
||||
* Options:
|
||||
* - (Boolean) noRemote: defaults to true (-no-remote flag)
|
||||
* - (String) url: url to navigate to
|
||||
* - (String) profile: path to gecko profile
|
||||
* - (Array) argv: additional arguments to pass to process
|
||||
* - (Object) env: If present, specifies the exact environment to use.
|
||||
* - (Object) envOverrides: Changes to make to the current environment; a value
|
||||
* of null indicates that the key should be removed from the environment
|
||||
* entirely.
|
||||
*
|
||||
* var options = {}
|
||||
* mozrun(path, options, function(err, child, binary, argv) {
|
||||
*
|
||||
* });
|
||||
*/
|
||||
function run(path, options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
var spawnOpts = {
|
||||
cwd: undefined,
|
||||
env: process.env
|
||||
};
|
||||
|
||||
if (options) {
|
||||
if (options.env) {
|
||||
spawnOpts.env = options.env;
|
||||
} else if (options.envOverrides) {
|
||||
spawnOpts.env = {};
|
||||
var envKey, envVal;
|
||||
for (envKey in process.env) {
|
||||
spawnOpts.env[envKey] = process.env[envKey];
|
||||
}
|
||||
for (envKey in options.envOverrides) {
|
||||
envVal = options.envOverrides[envKey];
|
||||
// Treat undefineds like null since this makes more sense than ending up
|
||||
// with the string 'undefined' in the environment.
|
||||
if (envVal == null) {
|
||||
delete spawnOpts.env[envKey];
|
||||
}
|
||||
else {
|
||||
spawnOpts.env[envKey] = envVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// handler for creating process
|
||||
function spawnProduct(binPath) {
|
||||
var argv = buildArgv(options);
|
||||
debug('arv:', argv);
|
||||
debug('bin:', binPath);
|
||||
// spawn child process
|
||||
callback(null, spawn(binPath, argv, spawnOpts), binPath, argv);
|
||||
}
|
||||
|
||||
function foundBinary(err, binPath) {
|
||||
if (err) return callback(err);
|
||||
debug('found bin', binPath);
|
||||
spawnProduct(binPath);
|
||||
}
|
||||
|
||||
if (options && options.runtime &&
|
||||
fs.existsSync(options.runtime)) {
|
||||
foundBinary(null, options.runtime);
|
||||
} else {
|
||||
detectBinary(path, options, foundBinary);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.run = run;
|
||||
12
gaia/tests/jsmarionette/runner/mozilla-runner/package.json
Executable file
12
gaia/tests/jsmarionette/runner/mozilla-runner/package.json
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "mozilla-runner",
|
||||
"version": "0.5.0",
|
||||
"author": "James Lal [:lightsofapollo]",
|
||||
"description": "Firefox / B2G runtime manager.",
|
||||
"main": "index.js",
|
||||
|
||||
"dependencies" : {
|
||||
"debug": "*",
|
||||
"mozilla-profile-builder": "file:../mozilla-profile-builder"
|
||||
}
|
||||
}
|
||||
65
gaia/tests/jsmarionette/runner/mozilla-runner/test/detectbinary.js
Executable file
65
gaia/tests/jsmarionette/runner/mozilla-runner/test/detectbinary.js
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
var fsPath = require('path');
|
||||
var subject = require('../lib/detectbinary').detectBinary;
|
||||
|
||||
suite('detectbinary', function() {
|
||||
var platforms = {
|
||||
mac: {
|
||||
path: __dirname + '/fixtures/product-mac',
|
||||
suffix: '/Contents/MacOS',
|
||||
platform: 'darwin',
|
||||
product: 'firefox'
|
||||
},
|
||||
noBinMac: {
|
||||
path: __dirname + '/fixtures/product-mac-no-bin',
|
||||
suffix: '/Contents/MacOS',
|
||||
platform: 'darwin',
|
||||
product: 'firefox'
|
||||
},
|
||||
linux: {
|
||||
path: __dirname + '/fixtures/product-linux',
|
||||
suffix: '',
|
||||
platform: 'linux-bs',
|
||||
product: 'firefox'
|
||||
},
|
||||
noBinLinux: {
|
||||
path: __dirname + '/fixtures/product-linux-no-bin',
|
||||
suffix: '',
|
||||
platform: 'linux-bs',
|
||||
product: 'firefox'
|
||||
}
|
||||
};
|
||||
|
||||
function fixturePath(fixture, bin) {
|
||||
fsPath.join(
|
||||
fixture.path,
|
||||
fixture.suffix,
|
||||
bin
|
||||
);
|
||||
}
|
||||
|
||||
function verify(name, fixture, bin) {
|
||||
test(name, function(done) {
|
||||
subject(fixture.path, fixture, function(err, path) {
|
||||
if (err) return done(err);
|
||||
assert.equal(fixturePath(fixture, bin));
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
verify('mac with -bin', platforms.mac, 'firefox-bin');
|
||||
verify('mac without -bin', platforms.noBinMac, 'firefox');
|
||||
verify('linux with -bin', platforms.linux, 'firefox-bin');
|
||||
verify('linux without -bin', platforms.noBinLinux, 'firefox');
|
||||
|
||||
test('missing binary', function(done) {
|
||||
subject(__dirname, { product: 'firefox', bin: 'nothere' }, function(err) {
|
||||
assert.ok(err, 'has error');
|
||||
assert.ok(err.message.indexOf(__dirname) !== 0, 'contains path');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
3
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/env-echoer/echostuff
vendored
Executable file
3
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/env-echoer/echostuff
vendored
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo FOO is $FOO and BAR is $BAR stop
|
||||
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-linux-no-bin/firefox
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-linux-no-bin/firefox
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-linux/firefox-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-linux/firefox-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-linux/foo-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-linux/foo-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac-no-bin/Contents/MacOS/firefox
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac-no-bin/Contents/MacOS/firefox
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac-no-bin/Contents/MacOS/foo-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac-no-bin/Contents/MacOS/foo-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac/Contents/MacOS/firefox
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac/Contents/MacOS/firefox
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac/Contents/MacOS/firefox-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac/Contents/MacOS/firefox-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac/Contents/MacOS/foo-bin
vendored
Executable file
0
gaia/tests/jsmarionette/runner/mozilla-runner/test/fixtures/product-mac/Contents/MacOS/foo-bin
vendored
Executable file
13
gaia/tests/jsmarionette/runner/mozilla-runner/test/html/dump.html
Executable file
13
gaia/tests/jsmarionette/runner/mozilla-runner/test/html/dump.html
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<title>Dump</title>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
dump('\n\n WOOT DUMP \n\n');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
189
gaia/tests/jsmarionette/runner/mozilla-runner/test/run.js
Executable file
189
gaia/tests/jsmarionette/runner/mozilla-runner/test/run.js
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
'use strict';
|
||||
var Static = require('node-static');
|
||||
var assert = require('assert');
|
||||
var profile = require('mozilla-profile-builder');
|
||||
var run = require('../lib/run').run;
|
||||
|
||||
var runtime = __dirname + '/../../../../../firefox';
|
||||
|
||||
'use strict';
|
||||
suite('run', function() {
|
||||
test('launch mulet', function(done) {
|
||||
var options = { product: 'firefox', profile: process.env.PROFILE_FOLDER };
|
||||
this.timeout(5000);
|
||||
run(runtime, options, function(err, child, bin, argv) {
|
||||
assert.ok(!err, err && err.message);
|
||||
|
||||
// verify the binary
|
||||
assert.ok(bin.indexOf('firefox-bin') !== 0, 'has bin');
|
||||
|
||||
// verify arguments
|
||||
assert.deepEqual(
|
||||
argv,
|
||||
['-profile', process.env.PROFILE_FOLDER, '-no-remote']
|
||||
);
|
||||
|
||||
// verify we have a child
|
||||
assert.ok(child.kill, 'is process');
|
||||
child.kill();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('launch mulet with different screen', function(done) {
|
||||
var options =
|
||||
{ product: 'firefox', profile: process.env.PROFILE_FOLDER,
|
||||
screen: {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
dpi: 160
|
||||
}
|
||||
};
|
||||
this.timeout(5000);
|
||||
run(runtime, options, function(err, child, bin, argv) {
|
||||
assert.ok(!err, err && err.message);
|
||||
|
||||
// verify screen size
|
||||
assert.deepEqual(
|
||||
argv,
|
||||
[
|
||||
'-profile', process.env.PROFILE_FOLDER, '--screen=1280x800@160',
|
||||
'-no-remote'
|
||||
]
|
||||
);
|
||||
|
||||
// verify we have a child
|
||||
assert.ok(child.kill, 'is process');
|
||||
child.kill();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('check env overrides', function(done) {
|
||||
// this should not be seen in the child since we are deleting it
|
||||
process.env.BAR = 'bar';
|
||||
var options = {
|
||||
product: 'echostuff',
|
||||
envOverrides: {
|
||||
FOO: 'foo',
|
||||
BAR: null
|
||||
}
|
||||
};
|
||||
var expected = 'FOO is foo and BAR is stop';
|
||||
this.timeout(5000);
|
||||
run(__dirname + '/fixtures/env-echoer', options,
|
||||
function(err, child, bin, argv) {
|
||||
assert.ok(!err, err && err.message);
|
||||
|
||||
child.stdout.on('data', function(content) {
|
||||
content = content.toString();
|
||||
// verify we can go to a given url
|
||||
if (content.indexOf(expected) !== -1) {
|
||||
child.kill();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('explicit env', function(done) {
|
||||
// this should not be seen in the child since it is using an explicit env.
|
||||
process.env.BAR = 'bar';
|
||||
var options = {
|
||||
product: 'echostuff',
|
||||
env: {
|
||||
FOO: 'foo',
|
||||
}
|
||||
};
|
||||
var expected = 'FOO is foo and BAR is stop';
|
||||
this.timeout(5000);
|
||||
run(__dirname + '/fixtures/env-echoer', options,
|
||||
function(err, child, bin, argv) {
|
||||
assert.ok(!err, err && err.message);
|
||||
|
||||
child.stdout.on('data', function(content) {
|
||||
content = content.toString();
|
||||
if (content.indexOf(expected) !== -1) {
|
||||
child.kill();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('env is expected default if not specified', function(done) {
|
||||
process.env.FOO = 'foo';
|
||||
process.env.BAR = 'bar';
|
||||
var options = {
|
||||
product: 'echostuff',
|
||||
};
|
||||
var expected = 'FOO is foo and BAR is bar stop';
|
||||
this.timeout(5000);
|
||||
run(__dirname + '/fixtures/env-echoer', options,
|
||||
function(err, child, bin, argv) {
|
||||
assert.ok(!err, err && err.message);
|
||||
|
||||
child.stdout.on('data', function(content) {
|
||||
content = content.toString();
|
||||
if (content.indexOf(expected) !== -1) {
|
||||
child.kill();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('launch mulet read dump', function() {
|
||||
var server;
|
||||
var port = 60033;
|
||||
var config = {
|
||||
// turn on dump
|
||||
'browser.dom.window.dump.enabled': true
|
||||
};
|
||||
|
||||
// create server
|
||||
setup(function() {
|
||||
var file = new (Static.Server)(__dirname + '/html');
|
||||
server = require('http').createServer(function(req, res) {
|
||||
// stolen from node-Static
|
||||
req.on('end', function() {
|
||||
file.serve(req, res);
|
||||
}).resume();
|
||||
});
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
// ensure server gets closed
|
||||
teardown(function() {
|
||||
server.close();
|
||||
});
|
||||
|
||||
// create tmp profile
|
||||
var profileDir;
|
||||
setup(function(done) {
|
||||
profile.create({ prefs: config }, function(err, result) {
|
||||
if (err) return done(err);
|
||||
profileDir = result.path;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var url = 'http://localhost:' + port + '/dump.html';
|
||||
var expected = 'WOOT DUMP';
|
||||
test('go to url', function(done) {
|
||||
var options = { product: 'firefox', profile: profileDir, url: url };
|
||||
run(runtime, options, function(err, child) {
|
||||
if (err) return done(err);
|
||||
child.stdout.on('data', function(content) {
|
||||
content = content.toString();
|
||||
// verify we can go to a given url
|
||||
if (content.indexOf(expected) !== -1) {
|
||||
child.kill();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue