mirror of https://github.com/embarklabs/embark.git
chore: test framework (#1894)
* chore: test framework * chore(@embark/testing): introduce Plugin apis and other changes * refactor(@embark/deployment): use new testing APIs for tests
This commit is contained in:
parent
009960e51a
commit
2f9d5e6085
|
@ -0,0 +1,4 @@
|
|||
engine-strict = true
|
||||
package-lock = false
|
||||
save-exact = true
|
||||
scripts-prepend-node-path = true
|
|
@ -0,0 +1,7 @@
|
|||
testing
|
||||
=======
|
||||
|
||||
> Testing
|
||||
|
||||
Visit [embark.status.im](https://embark.status.im/) to get started with
|
||||
[Embark](https://github.com/embark-framework/embark).
|
|
@ -0,0 +1,42 @@
|
|||
const cloneDeep = require('lodash.clonedeep');
|
||||
|
||||
module.exports = api => {
|
||||
const env = api.env();
|
||||
|
||||
const base = {
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
[
|
||||
'@babel/plugin-transform-runtime',
|
||||
{
|
||||
corejs: 2
|
||||
}
|
||||
]
|
||||
],
|
||||
presets: ['@babel/preset-env']
|
||||
};
|
||||
|
||||
if (env === 'base' || env.startsWith('base:')) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const node = cloneDeep(base);
|
||||
node.presets[0] = [
|
||||
node.presets[0],
|
||||
{
|
||||
targets: { node: '8.11.3' }
|
||||
}
|
||||
];
|
||||
|
||||
if (env === 'node' || env.startsWith('node:')) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const test = cloneDeep(node);
|
||||
|
||||
if (env === 'test') {
|
||||
return test;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"name": "embark-testing",
|
||||
"version": "5.0.0",
|
||||
"description": "Testing",
|
||||
"main": "dist/lib/index.js",
|
||||
"repository": {
|
||||
"directory": "packages/testing",
|
||||
"type": "git",
|
||||
"url": "https://github.com/embark-framework/embark/"
|
||||
},
|
||||
"author": "Iuri Matias <iuri.matias@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/embark-framework/embark/issues",
|
||||
"keywords": [
|
||||
"blockchain",
|
||||
"dapps",
|
||||
"ethereum",
|
||||
"ipfs",
|
||||
"serverless",
|
||||
"solc",
|
||||
"solidity"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "cross-env BABEL_ENV=node babel src --extensions \".js\" --out-dir dist --root-mode upward --source-maps",
|
||||
"ci": "npm run qa",
|
||||
"clean": "npm run reset",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"lint:js": "eslint src/",
|
||||
"// lint:ts": "tslint -c tslint.json \"src/**/*.ts\"",
|
||||
"package": "npm pack",
|
||||
"// qa": "npm-run-all lint typecheck build package",
|
||||
"qa": "npm-run-all lint build package",
|
||||
"reset": "npx rimraf dist embark-*.tgz package",
|
||||
"start": "npm run watch",
|
||||
"// typecheck": "tsc",
|
||||
"watch": "run-p watch:*",
|
||||
"watch:build": "npm run build -- --verbose --watch",
|
||||
"// watch:typecheck": "npm run typecheck -- --preserveWatchOutput --watch",
|
||||
"test": "jest dist/test/**/*.js"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "../../.eslintrc.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"refute": "1.0.2",
|
||||
"sinon": "7.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.2.3",
|
||||
"@babel/core": "7.2.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.5.5",
|
||||
"@babel/plugin-transform-runtime": "7.5.5",
|
||||
"@babel/preset-env": "7.5.5",
|
||||
"@types/async": "2.0.50",
|
||||
"babel-jest": "24.9.0",
|
||||
"cross-env": "5.2.0",
|
||||
"eslint": "5.7.0",
|
||||
"jest": "24.9.0",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"rimraf": "2.6.3",
|
||||
"tslint": "5.16.0",
|
||||
"typescript": "3.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.12.0 <12.0.0",
|
||||
"npm": ">=6.4.1",
|
||||
"yarn": ">=1.12.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
const sinon = require('sinon');
|
||||
|
||||
class Embark {
|
||||
constructor(events, plugins) {
|
||||
this.events = events;
|
||||
this.plugins = plugins;
|
||||
this.config = {
|
||||
blockchainConfig: {}
|
||||
};
|
||||
|
||||
this.assert = new EmbarkAssert(this);
|
||||
|
||||
this.logger = {
|
||||
debug: sinon.fake(),
|
||||
info: sinon.fake(),
|
||||
warn: sinon.fake(),
|
||||
error: sinon.fake(),
|
||||
};
|
||||
}
|
||||
|
||||
registerActionForEvent(name, cb) {
|
||||
this.plugins.registerActionForEvent(name, cb);
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.plugins.teardown();
|
||||
}
|
||||
}
|
||||
|
||||
class EmbarkAssert {
|
||||
constructor(embark) {
|
||||
this.embark = embark;
|
||||
}
|
||||
|
||||
logged(level, message) {
|
||||
sinon.assert.calledWithMatch(this.embark.logger[level], message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Embark;
|
|
@ -0,0 +1,68 @@
|
|||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
|
||||
class Events {
|
||||
constructor() {
|
||||
this.commandHandlers = {};
|
||||
this.handlers = {};
|
||||
|
||||
this.assert = new EventsAssert(this);
|
||||
}
|
||||
|
||||
setCommandHandler(cmd, fn) {
|
||||
this.commandHandlers[cmd] = sinon.spy(fn);
|
||||
}
|
||||
|
||||
on(ev, cb) {
|
||||
if (!this.handlers[ev]) {
|
||||
this.handlers[ev] = [];
|
||||
}
|
||||
|
||||
this.handlers[ev].push(cb);
|
||||
}
|
||||
|
||||
emit() {
|
||||
|
||||
}
|
||||
|
||||
trigger(ev, ...args) {
|
||||
if (!this.handlers[ev]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlers[ev].forEach(h => h(...args));
|
||||
}
|
||||
|
||||
request(cmd, ...args) {
|
||||
assert(this.commandHandlers[cmd], `command handler for ${ cmd } not registered`);
|
||||
Promise.resolve(this.commandHandlers[cmd](...args));
|
||||
}
|
||||
|
||||
request2(cmd, ...args) {
|
||||
assert(this.commandHandlers[cmd], `command handler for ${ cmd } not registered`);
|
||||
this.commandHandlers[cmd](...args);
|
||||
}
|
||||
}
|
||||
|
||||
class EventsAssert {
|
||||
constructor(events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
commandHandlerRegistered(cmd) {
|
||||
assert(this.events.commandHandlers[cmd], `command handler for ${ cmd } wanted, but not registered`);
|
||||
}
|
||||
|
||||
commandHandlerCalled(cmd) {
|
||||
this.commandHandlerRegistered(cmd);
|
||||
sinon.assert.called(this.events.commandHandlers[cmd]);
|
||||
}
|
||||
|
||||
commandHandlerCalledWith(cmd, ...args) {
|
||||
this.commandHandlerRegistered(cmd);
|
||||
sinon.assert.calledWith(this.events.commandHandlers[cmd], ...args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Events;
|
|
@ -0,0 +1,22 @@
|
|||
const Embark = require('./embark');
|
||||
const Events = require('./events');
|
||||
const Plugins = require('./plugin');
|
||||
|
||||
const fakeEmbark = () => {
|
||||
const events = new Events();
|
||||
const plugins = new Plugins();
|
||||
|
||||
const embark = new Embark(events, plugins);
|
||||
return {
|
||||
embark,
|
||||
plugins
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Embark,
|
||||
Events,
|
||||
Plugins,
|
||||
|
||||
fakeEmbark
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
class Plugins {
|
||||
constructor() {
|
||||
this.plugin = new Plugin();
|
||||
}
|
||||
|
||||
createPlugin() {
|
||||
return this.plugin;
|
||||
}
|
||||
|
||||
emitAndRunActionsForEvent(name, options, callback) {
|
||||
const listeners = this.plugin.getListeners(name);
|
||||
if (listeners) {
|
||||
listeners.forEach(fn => fn(options, callback));
|
||||
}
|
||||
}
|
||||
|
||||
registerActionForEvent(name, cb) {
|
||||
this.plugin.registerActionForEvent(name, cb);
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.plugin.listeners = {};
|
||||
}
|
||||
}
|
||||
|
||||
class Plugin {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
getListeners(name) {
|
||||
return this.listeners[name];
|
||||
}
|
||||
|
||||
registerActionForEvent(name, action) {
|
||||
if (!this.listeners[name]) {
|
||||
this.listeners[name] = [];
|
||||
}
|
||||
this.listeners[name].push(action);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plugins;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/* globals describe, it */
|
||||
const assert = require('assert').strict;
|
||||
|
||||
describe('Testing', () => {
|
||||
it('should have tests', (done) => {
|
||||
assert(false, 'No tests yet on testing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../../tslint.json"
|
||||
}
|
|
@ -62,6 +62,7 @@
|
|||
"@babel/preset-env": "7.5.5",
|
||||
"babel-jest": "24.9.0",
|
||||
"cross-env": "5.2.0",
|
||||
"embark-testing": "^5.0.0",
|
||||
"eslint": "5.7.0",
|
||||
"jest": "24.9.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
|
|
|
@ -1,69 +1,12 @@
|
|||
import sinon from 'sinon';
|
||||
import assert from 'assert';
|
||||
import { fakeEmbark, Plugins } from 'embark-testing';
|
||||
import Deployment from '../src/';
|
||||
|
||||
const events = {
|
||||
listeners: {},
|
||||
setCommandHandler: sinon.spy((cmd, fn) => {
|
||||
events.listeners[cmd] = fn;
|
||||
}),
|
||||
request: sinon.spy((cmd, ...args) => {
|
||||
assert(events.listeners[cmd]);
|
||||
events.listeners[cmd](...args);
|
||||
}),
|
||||
emit: (name) => {}
|
||||
};
|
||||
|
||||
const plugins = {
|
||||
listeners: {},
|
||||
_plugin: {
|
||||
listeners: {},
|
||||
registerActionForEvent: (name, cb) => {
|
||||
if (!plugins._plugin.listeners[name]) {
|
||||
plugins._plugin.listeners[name] = [];
|
||||
}
|
||||
plugins._plugin.listeners[name].push(cb);
|
||||
}
|
||||
},
|
||||
createPlugin: (name, opts) => {
|
||||
return _plugin;
|
||||
},
|
||||
emitAndRunActionsForEvent: sinon.spy((name, options, cb) => {
|
||||
if (plugins._plugin.listeners[name]) {
|
||||
plugins._plugin.listeners[name].forEach(fn => {
|
||||
fn(options, cb);
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const fakeEmbark = {
|
||||
events,
|
||||
logger: {
|
||||
info: () => {}
|
||||
},
|
||||
config: {
|
||||
blockchainConfig: {}
|
||||
},
|
||||
assert: {
|
||||
hasCommandHandler: (cmd) => {
|
||||
sinon.assert.calledWith(fakeEmbark.events.setCommandHandler, cmd);
|
||||
},
|
||||
hasRequested: (cmd) => {
|
||||
sinon.assert.calledWith(fakeEmbark.events.request, cmd);
|
||||
},
|
||||
hasRequestedWith: (cmd, ...args) => {
|
||||
sinon.assert.calledWith(fakeEmbark.events.request, cmd, ...args);
|
||||
},
|
||||
},
|
||||
teardown: () => {
|
||||
fakeEmbark.events.listeners = {};
|
||||
plugins._plugin.listeners = {};
|
||||
}
|
||||
};
|
||||
|
||||
describe('stack/deployment', () => {
|
||||
|
||||
const { embark, plugins } = fakeEmbark();
|
||||
|
||||
let deployment;
|
||||
let deployedContracts = [];
|
||||
|
||||
|
@ -72,7 +15,7 @@ describe('stack/deployment', () => {
|
|||
let doneCb;
|
||||
|
||||
beforeEach(() => {
|
||||
deployment = new Deployment(fakeEmbark, { plugins });
|
||||
deployment = new Deployment(embark, { plugins });
|
||||
|
||||
beforeAllAction = sinon.spy((params, cb) => { cb(null, params); });
|
||||
beforeDeployAction = sinon.spy((params, cb) => { cb(null, params); });
|
||||
|
@ -90,7 +33,7 @@ describe('stack/deployment', () => {
|
|||
|
||||
afterEach(() => {
|
||||
deployedContracts = [];
|
||||
fakeEmbark.teardown();
|
||||
embark.teardown();
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
|
@ -98,12 +41,12 @@ describe('stack/deployment', () => {
|
|||
|
||||
let testContract = { className: 'TestContract', shouldDeploy: true };
|
||||
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:beforeDeploy', beforeDeployAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:shouldDeploy', shouldDeployAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:deployed', deployedAction);
|
||||
embark.registerActionForEvent('deployment:contract:beforeDeploy', beforeDeployAction);
|
||||
embark.registerActionForEvent('deployment:contract:shouldDeploy', shouldDeployAction);
|
||||
embark.registerActionForEvent('deployment:contract:deployed', deployedAction);
|
||||
|
||||
events.request('deployment:deployer:register', 'ethereum', deployFn);
|
||||
events.request('deployment:contract:deploy', testContract, doneCb);
|
||||
embark.events.request('deployment:deployer:register', 'ethereum', deployFn);
|
||||
embark.events.request('deployment:contract:deploy', testContract, doneCb);
|
||||
|
||||
assert(beforeDeployAction.calledOnce)
|
||||
assert(shouldDeployAction.calledOnce)
|
||||
|
@ -120,20 +63,20 @@ describe('stack/deployment', () => {
|
|||
{ className: 'Contract3', shouldDeploy: true }
|
||||
];
|
||||
|
||||
plugins._plugin.registerActionForEvent('deployment:deployContracts:beforeAll', beforeAllAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:beforeDeploy', beforeDeployAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:shouldDeploy', shouldDeployAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:deployed', deployedAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:deployContracts:afterAll', afterAllAction);
|
||||
embark.registerActionForEvent('deployment:deployContracts:beforeAll', beforeAllAction);
|
||||
embark.registerActionForEvent('deployment:contract:beforeDeploy', beforeDeployAction);
|
||||
embark.registerActionForEvent('deployment:contract:shouldDeploy', shouldDeployAction);
|
||||
embark.registerActionForEvent('deployment:contract:deployed', deployedAction);
|
||||
embark.registerActionForEvent('deployment:deployContracts:afterAll', afterAllAction);
|
||||
|
||||
events.request('deployment:deployer:register', 'ethereum', deployFn);
|
||||
events.request('deployment:contracts:deploy', testContracts, {}, doneCb);
|
||||
embark.events.request('deployment:deployer:register', 'ethereum', deployFn);
|
||||
embark.events.request('deployment:contracts:deploy', testContracts, {}, doneCb);
|
||||
|
||||
assert(beforeAllAction.calledOnce);
|
||||
|
||||
fakeEmbark.assert.hasRequestedWith('deployment:contract:deploy', testContracts[0]);
|
||||
fakeEmbark.assert.hasRequestedWith('deployment:contract:deploy', testContracts[1]);
|
||||
fakeEmbark.assert.hasRequestedWith('deployment:contract:deploy', testContracts[2]);
|
||||
embark.events.assert.commandHandlerCalledWith('deployment:contract:deploy', testContracts[0]);
|
||||
embark.events.assert.commandHandlerCalledWith('deployment:contract:deploy', testContracts[1]);
|
||||
embark.events.assert.commandHandlerCalledWith('deployment:contract:deploy', testContracts[2]);
|
||||
|
||||
assert(deployFn.calledWith(testContracts[0]));
|
||||
assert(deployFn.calledWith(testContracts[1]));
|
||||
|
@ -164,14 +107,14 @@ describe('stack/deployment', () => {
|
|||
F: []
|
||||
};
|
||||
|
||||
plugins._plugin.registerActionForEvent('deployment:deployContracts:beforeAll', beforeAllAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:beforeDeploy', beforeDeployAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:shouldDeploy', shouldDeployAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:contract:deployed', deployedAction);
|
||||
plugins._plugin.registerActionForEvent('deployment:deployContracts:afterAll', afterAllAction);
|
||||
embark.registerActionForEvent('deployment:deployContracts:beforeAll', beforeAllAction);
|
||||
embark.registerActionForEvent('deployment:contract:beforeDeploy', beforeDeployAction);
|
||||
embark.registerActionForEvent('deployment:contract:shouldDeploy', shouldDeployAction);
|
||||
embark.registerActionForEvent('deployment:contract:deployed', deployedAction);
|
||||
embark.registerActionForEvent('deployment:deployContracts:afterAll', afterAllAction);
|
||||
|
||||
events.request('deployment:deployer:register', 'ethereum', deployFn);
|
||||
events.request('deployment:contracts:deploy', testContracts, testContractDependencies, doneCb);
|
||||
embark.events.request('deployment:deployer:register', 'ethereum', deployFn);
|
||||
embark.events.request('deployment:contracts:deploy', testContracts, testContractDependencies, doneCb);
|
||||
|
||||
assert.equal(deployedContracts.length, 6);
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"async": "2.6.1",
|
||||
"chalk": "2.4.2",
|
||||
"embark-i18n": "^4.1.1",
|
||||
"embark-testing": "5.0.0",
|
||||
"embark-utils": "^4.1.1",
|
||||
"fs-extra": "7.0.1",
|
||||
"istanbul-lib-coverage": "2.0.5",
|
||||
|
|
|
@ -2,17 +2,17 @@ const assert = require('assert').strict;
|
|||
const refute = require('refute')(assert);
|
||||
const sinon = require('sinon');
|
||||
|
||||
const {fakeEmbark} = require('embark-testing');
|
||||
|
||||
const TestRunner = require('../lib/index.js');
|
||||
|
||||
describe('Test Runner', () => {
|
||||
let embark;
|
||||
let _embark;
|
||||
let instance;
|
||||
|
||||
beforeEach(() => {
|
||||
const events = { setCommandHandler: () => {}, on: () => {} };
|
||||
const logger = { warn: sinon.fake() };
|
||||
|
||||
embark = { events, logger };
|
||||
const { embark } = fakeEmbark();
|
||||
_embark = embark;
|
||||
instance = new TestRunner(embark, {});
|
||||
});
|
||||
|
||||
|
@ -36,6 +36,7 @@ describe('Test Runner', () => {
|
|||
};
|
||||
|
||||
instance.runners = [first, second];
|
||||
|
||||
instance.getFilesFromDir = (_, cb) => {
|
||||
cb(null, ['test/file_first.js', 'test/file_second.js']);
|
||||
};
|
||||
|
@ -54,7 +55,7 @@ describe('Test Runner', () => {
|
|||
sinon.assert.calledWith(second.matchFn, 'luri.js');
|
||||
|
||||
// Ensure that we logged
|
||||
sinon.assert.calledWithMatch(embark.logger.warn, /luri.js/);
|
||||
_embark.assert.logged('warn', /luri.js/);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -66,7 +67,7 @@ describe('Test Runner', () => {
|
|||
refute(err);
|
||||
|
||||
// Ensure that we didn't warn that runners weren't registered.
|
||||
sinon.assert.notCalled(embark.logger.warn);
|
||||
sinon.assert.notCalled(_embark.logger.warn);
|
||||
|
||||
// Ensure plugins received the correct files
|
||||
sinon.assert.calledWith(first.addFn, 'test/file_first.js');
|
||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -18415,6 +18415,13 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@2.6.3, rimraf@~2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b"
|
||||
|
@ -18422,13 +18429,6 @@ rimraf@3.0.0:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@~2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
|
||||
|
|
Loading…
Reference in New Issue