Progress
This commit is contained in:
parent
253d3dd57c
commit
8e396a17d8
|
@ -1,22 +1,37 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ContractSource = require('./contract_source');
|
||||
|
||||
class ContractSources {
|
||||
constructor(files) {
|
||||
if(!Array.isArray(files))
|
||||
files = [files];
|
||||
|
||||
this.files = {};
|
||||
|
||||
files.forEach((file) => {
|
||||
try {
|
||||
var content = fs.readFileSync(file).toString()
|
||||
this.files[file] = new ContractSource(file, content);
|
||||
} catch(e) {
|
||||
throw new Error(`Error loading ${file}: ${e.code}`)
|
||||
}
|
||||
});
|
||||
switch(Object.prototype.toString.call(files)) {
|
||||
case '[object Object]':
|
||||
Object.keys(files).forEach((file) => {
|
||||
var basename = path.basename(file);
|
||||
this.files[basename] = new ContractSource(basename, files[file]);
|
||||
});
|
||||
|
||||
break;
|
||||
case '[object String]':
|
||||
// No 'break' statement here on purpose, as it shares
|
||||
// the logic below.
|
||||
files = [files];
|
||||
|
||||
case '[object Array]':
|
||||
files.forEach((file) => {
|
||||
var basename = path.basename(file);
|
||||
try {
|
||||
var content = fs.readFileSync(file).toString();
|
||||
this.files[basename] = new ContractSource(basename, content);
|
||||
} catch(e) {
|
||||
throw new Error(`Error loading ${file}: ${e.code}`);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toSolcInputs() {
|
||||
|
@ -31,8 +46,14 @@ class ContractSources {
|
|||
|
||||
parseSolcOutput(output) {
|
||||
for(var file in output.contracts) {
|
||||
var contractSource = this.files[file];
|
||||
contractSource.parseSolcOutput(output.sources[file], output.contracts[file])
|
||||
var basename = path.basename(file);
|
||||
var contractSource = this.files[basename];
|
||||
if(!contractSource){
|
||||
continue; // TODO CHECK THIS LOGIC
|
||||
throw new Error(`Can't attribute output to ${file}: file has not been read in.`);
|
||||
}
|
||||
|
||||
contractSource.parseSolcOutput(output.sources[file], output.contracts[file]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,7 +65,33 @@ class ContractSources {
|
|||
coverageReport[file] = contractSource.generateCodeCoverage(trace);
|
||||
}
|
||||
|
||||
return coverageReport;
|
||||
if(!this.coverageReport) {
|
||||
this.coverageReport = coverageReport;
|
||||
return this.coverageReport;
|
||||
}
|
||||
|
||||
// We already have a previous coverage report, so we're merging results here.
|
||||
Object.keys(coverageReport).forEach((file) => {
|
||||
if(!this.coverageReport[file]) {
|
||||
this.coverageReport[file] = coverageReport[file];
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment counters for statements, functions and lines
|
||||
['s', 'f', 'l'].forEach((countType) => {
|
||||
Object.keys(coverageReport[file][countType]).forEach((id) => {
|
||||
this.coverageReport[file][countType][id] += coverageReport[file][countType][id];
|
||||
});
|
||||
});
|
||||
|
||||
// Branch counts are tracked in a different manner so we'll do these now
|
||||
Object.keys(coverageReport[file].b).forEach((id) => {
|
||||
this.coverageReport[file].b[id][0] += coverageReport[file].b[id][0];
|
||||
this.coverageReport[file].b[id][1] += coverageReport[file].b[id][1];
|
||||
});
|
||||
});
|
||||
|
||||
return this.coverageReport;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,36 @@
|
|||
const ContractSources = require('./contract_sources');
|
||||
|
||||
class CodeCoverage {
|
||||
constructor(embark, _options) {
|
||||
this.events = embark.events;
|
||||
this.logger = embark.logger;
|
||||
|
||||
embark.events.on('contracts:compile:solc', this.compileSolc.bind(this));
|
||||
embark.events.on('contracts:compiled:solc', this.compiledSolc.bind(this));
|
||||
embark.events.on('contracts:run:solc', this.runSolc.bind(this));
|
||||
embark.registerActionForEvent('contracts:deploy:afterAll', this.deployed.bind(this));
|
||||
|
||||
embark.events.on('block:header', this.runSolc.bind(this));
|
||||
}
|
||||
|
||||
compileSolc(input) {
|
||||
var sources = {};
|
||||
|
||||
Object.keys(input.sources).forEach((path) => {
|
||||
sources[path] = input.sources[path].content;
|
||||
});
|
||||
|
||||
this.contractSources = new ContractSources(sources);
|
||||
}
|
||||
|
||||
compiledSolc(output) {
|
||||
this.contractSources.parseSolcOutput(output);
|
||||
}
|
||||
|
||||
runSolc(receipt) {
|
||||
console.log('runSolc');
|
||||
console.dir(receipt);
|
||||
//this.contractSources.generateCodeCoverage(trace);
|
||||
}
|
||||
|
||||
deployed(cb) {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
class SourceMap {
|
||||
constructor(sourceMapStringOrOffset, length, id) {
|
||||
if(typeof sourceMapStringOrOffset == 'string') {
|
||||
var [offset, length, id, ..._rest] = sourceMapStringOrOffset.split(":");
|
||||
|
||||
this.offset = parseInt(offset, 10);
|
||||
this.length = parseInt(length, 10);
|
||||
|
||||
if(id) this.id = parseInt(id, 10);
|
||||
} else {
|
||||
this.offset = sourceMapStringOrOffset;
|
||||
this.length = length;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
subtract(sourceMap) {
|
||||
var length = sourceMap.offset - this.offset;
|
||||
return new SourceMap(this.offset, length);
|
||||
}
|
||||
|
||||
toString() {
|
||||
var parts = [this.offset, this.length];
|
||||
if(this.id) parts.push(this.id);
|
||||
|
||||
return parts.join(':');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SourceMap;
|
|
@ -200,6 +200,7 @@ class ContractDeployer {
|
|||
}
|
||||
// saving code changes back to the contract object
|
||||
contract.code = contractCode;
|
||||
self.events.request('contracts:setBytecode', contract.className, contractCode);
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -37,7 +37,7 @@ class Solidity {
|
|||
self.logger.error(__('Error while loading the content of ') + filename);
|
||||
return fileCb();
|
||||
}
|
||||
input[filename] = {content: fileContent.replace(/\r\n/g, '\n')};
|
||||
input[filename] = {content: fileContent.replace(/\r\n/g, '\n'), path: file.path};
|
||||
fileCb();
|
||||
});
|
||||
},
|
||||
|
@ -70,16 +70,34 @@ class Solidity {
|
|||
},
|
||||
outputSelection: {
|
||||
'*': {
|
||||
'*': ['abi', 'metadata', 'userdoc', 'devdoc', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates']
|
||||
'': [
|
||||
'ast',
|
||||
'legacyAST'
|
||||
],
|
||||
'*': [
|
||||
'abi',
|
||||
'devdoc',
|
||||
'evm.bytecode',
|
||||
'evm.deployedBytecode',
|
||||
'evm.gasEstimates',
|
||||
'evm.legacyAssembly',
|
||||
'evm.methodIdentifiers',
|
||||
'metadata',
|
||||
'userdoc'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.solcW.compile(jsonObj, function (err, output) {
|
||||
self.events.emit('contracts:compile:solc', jsonObj);
|
||||
|
||||
if(err){
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
output.errors = []; // TODO REMOVE THIS
|
||||
if (output.errors) {
|
||||
for (let i=0; i<output.errors.length; i++) {
|
||||
if (output.errors[i].type === 'Warning') {
|
||||
|
@ -90,6 +108,10 @@ class Solidity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
//self.plugins.emitAndRunActionsForEvent('contracts:compiled:solc', output);
|
||||
self.events.emit('contracts:compiled:solc', output);
|
||||
|
||||
callback(null, output);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1490,6 +1490,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"assertion-error": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
|
||||
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
|
||||
"dev": true
|
||||
},
|
||||
"assign-symbols": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
|
||||
|
@ -2849,6 +2855,20 @@
|
|||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz",
|
||||
"integrity": "sha512-vs79o1mOSKRGv/1pSkp4EXgl4ZviWeYReXw60XfacPU64uQWZwJT6vZNmxRF9O+6zu71sJwMxLK5JXxbzuVrLw=="
|
||||
},
|
||||
"chai": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
|
||||
"integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"assertion-error": "^1.0.1",
|
||||
"check-error": "^1.0.1",
|
||||
"deep-eql": "^3.0.0",
|
||||
"get-func-name": "^2.0.0",
|
||||
"pathval": "^1.0.0",
|
||||
"type-detect": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
|
@ -2866,6 +2886,12 @@
|
|||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
|
||||
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
|
||||
},
|
||||
"check-error": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
||||
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
|
||||
"dev": true
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
|
||||
|
@ -3625,6 +3651,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"deep-eql": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
|
||||
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||
|
@ -5095,6 +5130,12 @@
|
|||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
|
||||
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
|
||||
},
|
||||
"get-func-name": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
|
||||
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||
"dev": true
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
|
@ -8835,6 +8876,12 @@
|
|||
"pify": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pathval": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
|
||||
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
|
||||
"dev": true
|
||||
},
|
||||
"pbkdf2": {
|
||||
"version": "3.0.16",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
"devDependencies": {
|
||||
"eslint": "4.13.1",
|
||||
"mocha-sinon": "^1.1.4",
|
||||
"sinon": "^4.5.0"
|
||||
"sinon": "^4.5.0",
|
||||
"chai": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
const {assert, expect} = require('chai');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const ContractSources = require('../lib/modules/coverage/contract_sources');
|
||||
const ContractSource = require('../lib/modules/coverage/contract_source');
|
||||
const SourceMap = require('../lib/modules/coverage/source_map');
|
||||
|
||||
function fixturePath(fixture) {
|
||||
return path.join(__dirname, 'fixtures', fixture);
|
||||
}
|
||||
|
||||
function loadFixture(fixture) {
|
||||
return fs.readFileSync(fixturePath(fixture)).toString();
|
||||
}
|
||||
|
||||
function dumpToFile(obj, path) {
|
||||
return fs.writeFileSync(path, JSON.stringify(obj));
|
||||
}
|
||||
|
||||
describe('ContractSources', () => {
|
||||
describe('constructor', () => {
|
||||
it('should read files and create instances of ContractSource', (done) => {
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources([contractPath]);
|
||||
assert.instanceOf(cs.files['cont.sol'], ContractSource);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should work when a single path is passed', (done) => {
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources(contractPath);
|
||||
assert.instanceOf(cs.files['cont.sol'], ContractSource);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw an error when the file does not exist', (done) => {
|
||||
assert.throws(() => {
|
||||
new ContractSources(['fixtures/404.sol'])
|
||||
}, 'Error loading fixtures/404.sol: ENOENT');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toSolcInputs', () => {
|
||||
it('should build the hash in the format that solc likes', (done) => {
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources([contractPath]);
|
||||
assert.deepEqual({
|
||||
'cont.sol': {content: cs.files['cont.sol'].body}
|
||||
}, cs.toSolcInputs());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#parseSolcOutput', () => {
|
||||
it('should send the output to each of the ContractSource instances', (done) => {
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources([contractPath]);
|
||||
|
||||
var parseSolcOutputSpy = sinon.spy(cs.files['cont.sol'], 'parseSolcOutput');
|
||||
const solcOutput = JSON.parse(loadFixture('solc-output.json'));
|
||||
cs.parseSolcOutput(solcOutput);
|
||||
|
||||
assert(parseSolcOutputSpy.calledOnce);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ContractSource', () => {
|
||||
const contractSource = `
|
||||
pragma solidity ^0.4.24;
|
||||
|
||||
contract x {
|
||||
int number;
|
||||
string name;
|
||||
|
||||
constructor(string _name)
|
||||
public
|
||||
{
|
||||
name = _name;
|
||||
}
|
||||
|
||||
function g(int _number)
|
||||
public
|
||||
returns (int _multiplication)
|
||||
{
|
||||
number = _number;
|
||||
return _number * 5;
|
||||
}
|
||||
|
||||
function f(int _foo, int _bar)
|
||||
public
|
||||
pure
|
||||
returns (int _addition)
|
||||
{
|
||||
return _foo + _bar;
|
||||
}
|
||||
|
||||
function h(int _bar)
|
||||
public
|
||||
pure
|
||||
returns (bool _great)
|
||||
{
|
||||
if(_bar > 25) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
|
||||
const cs = new ContractSource('contract.sol', contractSource);
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should set line offsets and line lengths correctly', (done) => {
|
||||
// +1 here accounts for a newline
|
||||
assert.equal("pragma solidity ^0.4.24;".length + 1, cs.lineOffsets[1]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sourceMapToLocations', () => {
|
||||
it('should return objects that indicate start and end location and columns', (done) => {
|
||||
// constructor function
|
||||
var loc = cs.sourceMapToLocations('71:60:0');
|
||||
assert.deepEqual({line: 7, column: 2}, loc.start);
|
||||
assert.deepEqual({line: 11, column: 3}, loc.end);
|
||||
|
||||
// f function
|
||||
loc = cs.sourceMapToLocations('257:104:0');
|
||||
assert.deepEqual({line: 21, column: 2}, loc.start);
|
||||
assert.deepEqual({line: 27, column: 3}, loc.end);
|
||||
|
||||
// g function
|
||||
loc = cs.sourceMapToLocations('135:118:0');
|
||||
assert.deepEqual({line: 13, column: 2}, loc.start);
|
||||
assert.deepEqual({line: 19, column: 3}, loc.end);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#parseSolcOutput', () => {
|
||||
it('should parse the bytecode output correctly', (done) => {
|
||||
var solcOutput = JSON.parse(loadFixture('solc-output.json'));
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources(contractPath);
|
||||
cs.parseSolcOutput(solcOutput);
|
||||
|
||||
var contractSource = cs.files['cont.sol'];
|
||||
|
||||
assert.isNotEmpty(contractSource.contractBytecode);
|
||||
assert.isNotEmpty(contractSource.contractBytecode['x']);
|
||||
|
||||
var bytecode = contractSource.contractBytecode['x'];
|
||||
|
||||
assert.deepEqual({instruction: 'PUSH1', sourceMap: '26:487:0:-', seen: false}, bytecode[0]);
|
||||
assert.deepEqual({instruction: 'PUSH1', sourceMap: '', seen: false}, bytecode[2]);
|
||||
assert.deepEqual({instruction: 'MSTORE', sourceMap: '', seen: false}, bytecode[4]);
|
||||
assert.deepEqual({instruction: 'PUSH1', sourceMap: '', seen: false}, bytecode[5]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateCodeCoverage', () => {
|
||||
it('should return an error when solc output was not parsed', (done) => {
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources(contractPath);
|
||||
var contractSource = cs.files['cont.sol'];
|
||||
var trace = JSON.parse(loadFixture('geth-debugtrace-output-g.json'));
|
||||
|
||||
assert.throws(() => {
|
||||
contractSource.generateCodeCoverage(trace);
|
||||
}, 'Error generating coverage: solc output was not assigned');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should return a coverage report when solc output was parsed', (done) => {
|
||||
var solcOutput = JSON.parse(loadFixture('solc-output.json'));
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources(contractPath);
|
||||
cs.parseSolcOutput(solcOutput);
|
||||
|
||||
var trace = JSON.parse(loadFixture('geth-debugtrace-output-h-5.json'));
|
||||
var coverage = cs.generateCodeCoverage(trace);
|
||||
dumpToFile(coverage, '/tmp/coverage.json');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should merge coverages as we add more traces', (done) => {
|
||||
const contractPath = fixturePath('cont.sol');
|
||||
var cs = new ContractSources(contractPath);
|
||||
|
||||
const solcOutput = JSON.parse(loadFixture('solc-output.json'));
|
||||
cs.parseSolcOutput(solcOutput);
|
||||
|
||||
var trace = JSON.parse(loadFixture('geth-debugtrace-output-h-5.json'));
|
||||
cs.generateCodeCoverage(trace);
|
||||
|
||||
trace = JSON.parse(loadFixture('geth-debugtrace-output-h-50.json'));
|
||||
var coverage = cs.generateCodeCoverage(trace)['cont.sol'];
|
||||
|
||||
// In the fixture, the branch has an ID of 61, and the function has the
|
||||
// ID of 63
|
||||
assert.deepEqual([1,1], coverage.b['61']);
|
||||
assert.equal(2, coverage.f['63']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SourceMap', () => {
|
||||
describe('#subtract', () => {
|
||||
it('should return the correct values', (done) => {
|
||||
var sm1 = new SourceMap('365:146:0');
|
||||
var sm2 = new SourceMap('428:83:0');
|
||||
|
||||
var result = sm1.subtract(sm2);
|
||||
|
||||
assert.equal(365, result.offset);
|
||||
assert.equal(63, result.length);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
pragma solidity ^0.4.24;
|
||||
|
||||
contract x {
|
||||
int number;
|
||||
string name;
|
||||
|
||||
constructor(string _name)
|
||||
public
|
||||
{
|
||||
name = _name;
|
||||
}
|
||||
|
||||
function g(int _number)
|
||||
public
|
||||
returns (int _multiplication)
|
||||
{
|
||||
number = _number;
|
||||
return _number * 5;
|
||||
}
|
||||
|
||||
function f(int _foo, int _bar)
|
||||
public
|
||||
pure
|
||||
returns (int _addition)
|
||||
{
|
||||
return _foo + _bar;
|
||||
}
|
||||
|
||||
function h(int _bar)
|
||||
public
|
||||
pure
|
||||
returns (bool _great)
|
||||
{
|
||||
if(_bar > 25) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue