Even faster tests (#4868)

* Various improvements in test name / de-duplications

* Reverts to class by class deletion, introduced fast mode that just delete data for mongo

- Speeds up are incredible Executed 1695 of 1713 specs INCOMPLETE (18 PENDING) in 4 mins 19 secs.

* Adds documentation about the deleteEverything
This commit is contained in:
Florent Vilmart
2018-07-03 11:13:08 -04:00
committed by GitHub
parent ae1a8226d5
commit 960431b92d
18 changed files with 648 additions and 651 deletions

View File

@@ -301,7 +301,7 @@ describe('AuthenticationProviders', function() {
}) })
}); });
it('properly loads custom adapter module object', (done) => { it('properly loads custom adapter module object (again)', (done) => {
const authenticationHandler = authenticationLoader({ const authenticationHandler = authenticationLoader({
customAuthentication: { module: path.resolve('./spec/support/CustomAuthFunction.js'), options: { token: 'valid-token' }} customAuthentication: { module: path.resolve('./spec/support/CustomAuthFunction.js'), options: { token: 'valid-token' }}
}); });

View File

@@ -132,21 +132,6 @@ describe("httpRequest", () => {
}); });
}) })
it("should fail on 404", (done) => {
httpRequest({
url: httpRequestServer + "/404",
}).then(function(){
fail("should not succeed");
done();
}, function(httpResponse){
expect(httpResponse.status).toBe(404);
expect(httpResponse.buffer).toEqual(new Buffer('NO'));
expect(httpResponse.text).toEqual('NO');
expect(httpResponse.data).toBe(undefined);
done();
})
})
it("should post on echo", (done) => { it("should post on echo", (done) => {
let calls = 0; let calls = 0;
httpRequest({ httpRequest({

View File

@@ -50,11 +50,11 @@ describe('JobSchedule', () => {
rp.put(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then(done.fail, () => done()); rp.put(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then(done.fail, () => done());
}); });
it('should reject access when not using masterKey (PUT /jobs/id)', (done) => { it('should reject access when not using masterKey (DELETE /jobs/id)', (done) => {
rp.del(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then(done.fail, () => done()); rp.del(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then(done.fail, () => done());
}); });
it('should allow access when using masterKey (/jobs)', (done) => { it('should allow access when using masterKey (GET /jobs)', (done) => {
rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions).then(done, done.fail); rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions).then(done, done.fail);
}); });

View File

@@ -2,7 +2,7 @@ const LoggerController = require('../lib/Controllers/LoggerController').LoggerCo
const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter;
describe('LoggerController', () => { describe('LoggerController', () => {
it('can check process a query without throwing', (done) => { it('can process an empty query without throwing', (done) => {
// Make mock request // Make mock request
const query = {}; const query = {};
@@ -37,7 +37,7 @@ describe('LoggerController', () => {
done(); done();
}); });
it('can process a query without throwing', (done) => { it('can process an ascending query without throwing', (done) => {
// Make mock request // Make mock request
const query = { const query = {
from: "2016-01-01Z00:00:00", from: "2016-01-01Z00:00:00",
@@ -58,7 +58,7 @@ describe('LoggerController', () => {
done(); done();
}); });
it('can check process a query without throwing', (done) => { it('can process a descending query without throwing', (done) => {
// Make mock request // Make mock request
const query = { const query = {
from: "2016-01-01", from: "2016-01-01",

View File

@@ -9,7 +9,7 @@ const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDataba
describe_only_db('mongo')('MongoStorageAdapter', () => { describe_only_db('mongo')('MongoStorageAdapter', () => {
beforeEach(done => { beforeEach(done => {
new MongoStorageAdapter({ uri: databaseURI }) new MongoStorageAdapter({ uri: databaseURI })
.dropDatabase() .deleteAllClasses()
.then(done, fail); .then(done, fail);
}); });

View File

@@ -24,7 +24,7 @@ describe('parseObjectToMongoObjectForCreate', () => {
done(); done();
}); });
it('built-in timestamps', (done) => { it('built-in timestamps with date', (done) => {
const input = { const input = {
createdAt: "2015-10-06T21:24:50.332Z", createdAt: "2015-10-06T21:24:50.332Z",
updatedAt: "2015-10-06T21:24:50.332Z" updatedAt: "2015-10-06T21:24:50.332Z"

View File

@@ -632,24 +632,6 @@ describe('Parse.File testing', () => {
}); });
}); });
it('fails to upload without a file name', done => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
};
request.post({
headers: headers,
url: 'http://localhost:8378/1/files/',
body: 'yolo',
}, (error, response, body) => {
expect(error).toBe(null);
expect(response.statusCode).toBe(400);
expect(body).toEqual('{"code":122,"error":"Filename not provided."}');
done();
});
});
it('fails to delete an unkown file', done => { it('fails to delete an unkown file', done => {
const headers = { const headers = {
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',

View File

@@ -336,7 +336,7 @@ describe('Hooks', () => {
}); });
}); });
it("should run the function on the test server", (done) => { it("should run the function on the test server (error handling)", (done) => {
app.post("/SomeFunctionError", function(req, res) { app.post("/SomeFunctionError", function(req, res) {
res.json({error: {code: 1337, error: "hacking that one!"}}); res.json({error: {code: 1337, error: "hacking that one!"}});

View File

@@ -8,6 +8,9 @@ const defaultHeaders = {
} }
describe('Parse.Polygon testing', () => { describe('Parse.Polygon testing', () => {
beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently());
it('polygon save open path', (done) => { it('polygon save open path', (done) => {
const coords = [[0,0],[0,1],[1,1],[1,0]]; const coords = [[0,0],[0,1],[1,1],[1,0]];
const closed = [[0,0],[0,1],[1,1],[1,0],[0,0]]; const closed = [[0,0],[0,1],[1,1],[1,0],[0,0]];
@@ -128,144 +131,150 @@ describe('Parse.Polygon testing', () => {
}, done.fail); }, done.fail);
}); });
it('polygonContain query', (done) => { describe('with location', () => {
const points1 = [[0,0],[0,1],[1,1],[1,0]]; beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently());
const points2 = [[0,0],[0,2],[2,2],[2,0]];
const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]];
const polygon1 = new Parse.Polygon(points1);
const polygon2 = new Parse.Polygon(points2);
const polygon3 = new Parse.Polygon(points3);
const obj1 = new TestObject({location: polygon1});
const obj2 = new TestObject({location: polygon2});
const obj3 = new TestObject({location: polygon3});
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
const where = {
location: {
$geoIntersects: {
$point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 }
}
}
};
return rp.post({
url: Parse.serverURL + '/classes/TestObject',
json: { where, '_method': 'GET' },
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
}).then((resp) => {
expect(resp.results.length).toBe(2);
done();
}, done.fail);
});
it('polygonContain query no reverse input (Regression test for #4608)', (done) => { it('polygonContain query', (done) => {
const points1 = [[.25,0],[.25,1.25],[.75,1.25],[.75,0]]; const points1 = [[0,0],[0,1],[1,1],[1,0]];
const points2 = [[0,0],[0,2],[2,2],[2,0]]; const points2 = [[0,0],[0,2],[2,2],[2,0]];
const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]]; const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]];
const polygon1 = new Parse.Polygon(points1); const polygon1 = new Parse.Polygon(points1);
const polygon2 = new Parse.Polygon(points2); const polygon2 = new Parse.Polygon(points2);
const polygon3 = new Parse.Polygon(points3); const polygon3 = new Parse.Polygon(points3);
const obj1 = new TestObject({location: polygon1}); const obj1 = new TestObject({location: polygon1});
const obj2 = new TestObject({location: polygon2}); const obj2 = new TestObject({location: polygon2});
const obj3 = new TestObject({location: polygon3}); const obj3 = new TestObject({location: polygon3});
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
const where = { const where = {
location: { location: {
$geoIntersects: { $geoIntersects: {
$point: { __type: 'GeoPoint', latitude: 0.5, longitude:1.0 } $point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 }
}
} }
} };
}; return rp.post({
return rp.post({ url: Parse.serverURL + '/classes/TestObject',
url: Parse.serverURL + '/classes/TestObject', json: { where, '_method': 'GET' },
json: { where, '_method': 'GET' }, headers: {
headers: { 'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Javascript-Key': Parse.javaScriptKey
'X-Parse-Javascript-Key': Parse.javaScriptKey }
} });
}); }).then((resp) => {
}).then((resp) => { expect(resp.results.length).toBe(2);
expect(resp.results.length).toBe(2); done();
done(); }, done.fail);
}, done.fail); });
});
it('polygonContain query real data (Regression test for #4608)', (done) => { it('polygonContain query no reverse input (Regression test for #4608)', (done) => {
const detroit = [[42.631655189280224,-83.78406753121705],[42.633047793854814,-83.75333640366955],[42.61625254348911,-83.75149921669944],[42.61526926650296,-83.78161794858735],[42.631655189280224,-83.78406753121705]]; const points1 = [[.25,0],[.25,1.25],[.75,1.25],[.75,0]];
const polygon = new Parse.Polygon(detroit); const points2 = [[0,0],[0,2],[2,2],[2,0]];
const obj = new TestObject({location: polygon}); const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]];
obj.save().then(() => { const polygon1 = new Parse.Polygon(points1);
const where = { const polygon2 = new Parse.Polygon(points2);
location: { const polygon3 = new Parse.Polygon(points3);
$geoIntersects: { const obj1 = new TestObject({location: polygon1});
$point: { __type: 'GeoPoint', latitude: 42.624599, longitude:-83.770162 } const obj2 = new TestObject({location: polygon2});
const obj3 = new TestObject({location: polygon3});
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
const where = {
location: {
$geoIntersects: {
$point: { __type: 'GeoPoint', latitude: 0.5, longitude:1.0 }
}
} }
} };
}; return rp.post({
return rp.post({ url: Parse.serverURL + '/classes/TestObject',
url: Parse.serverURL + '/classes/TestObject', json: { where, '_method': 'GET' },
json: { where, '_method': 'GET' }, headers: {
headers: { 'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Javascript-Key': Parse.javaScriptKey
'X-Parse-Javascript-Key': Parse.javaScriptKey }
} });
}); }).then((resp) => {
}).then((resp) => { expect(resp.results.length).toBe(2);
expect(resp.results.length).toBe(1); done();
done(); }, done.fail);
}, done.fail); });
});
it('polygonContain invalid input', (done) => { it('polygonContain query real data (Regression test for #4608)', (done) => {
const points = [[0,0],[0,1],[1,1],[1,0]]; const detroit = [[42.631655189280224,-83.78406753121705],[42.633047793854814,-83.75333640366955],[42.61625254348911,-83.75149921669944],[42.61526926650296,-83.78161794858735],[42.631655189280224,-83.78406753121705]];
const polygon = new Parse.Polygon(points); const polygon = new Parse.Polygon(detroit);
const obj = new TestObject({location: polygon}); const obj = new TestObject({location: polygon});
obj.save().then(() => { obj.save().then(() => {
const where = { const where = {
location: { location: {
$geoIntersects: { $geoIntersects: {
$point: { __type: 'GeoPoint', latitude: 181, longitude: 181 } $point: { __type: 'GeoPoint', latitude: 42.624599, longitude:-83.770162 }
}
} }
} };
}; return rp.post({
return rp.post({ url: Parse.serverURL + '/classes/TestObject',
url: Parse.serverURL + '/classes/TestObject', json: { where, '_method': 'GET' },
json: { where, '_method': 'GET' }, headers: {
headers: { 'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Javascript-Key': Parse.javaScriptKey
'X-Parse-Javascript-Key': Parse.javaScriptKey }
} });
}); }).then((resp) => {
}).then(done.fail, () => done()); expect(resp.results.length).toBe(1);
}); done();
}, done.fail);
});
it('polygonContain invalid geoPoint', (done) => { it('polygonContain invalid input', (done) => {
const points = [[0,0],[0,1],[1,1],[1,0]]; const points = [[0,0],[0,1],[1,1],[1,0]];
const polygon = new Parse.Polygon(points); const polygon = new Parse.Polygon(points);
const obj = new TestObject({location: polygon}); const obj = new TestObject({location: polygon});
obj.save().then(() => { obj.save().then(() => {
const where = { const where = {
location: { location: {
$geoIntersects: { $geoIntersects: {
$point: [] $point: { __type: 'GeoPoint', latitude: 181, longitude: 181 }
}
} }
} };
}; return rp.post({
return rp.post({ url: Parse.serverURL + '/classes/TestObject',
url: Parse.serverURL + '/classes/TestObject', json: { where, '_method': 'GET' },
json: { where, '_method': 'GET' }, headers: {
headers: { 'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Javascript-Key': Parse.javaScriptKey
'X-Parse-Javascript-Key': Parse.javaScriptKey }
} });
}); }).then(done.fail, () => done());
}).then(done.fail, () => done()); });
it('polygonContain invalid geoPoint', (done) => {
const points = [[0,0],[0,1],[1,1],[1,0]];
const polygon = new Parse.Polygon(points);
const obj = new TestObject({location: polygon});
obj.save().then(() => {
const where = {
location: {
$geoIntersects: {
$point: []
}
}
};
return rp.post({
url: Parse.serverURL + '/classes/TestObject',
json: { where, '_method': 'GET' },
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
}).then(done.fail, () => done());
});
}); });
}); });
describe_only_db('mongo')('Parse.Polygon testing', () => { describe_only_db('mongo')('Parse.Polygon testing', () => {
beforeEach(() => require('../lib/TestUtils').destroyAllDataPermanently());
it('support 2d and 2dsphere', (done) => { it('support 2d and 2dsphere', (done) => {
const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]]; const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]];
// testings against REST API, use raw formats // testings against REST API, use raw formats

View File

@@ -101,7 +101,7 @@ describe('Parse.User testing', () => {
}); });
}); });
it('user login with non-string username with REST API', (done) => { it('user login with non-string username with REST API (again)', (done) => {
Parse.User.signUp('asdf', 'zxcv', null, { Parse.User.signUp('asdf', 'zxcv', null, {
success: () => { success: () => {
return rp.post({ return rp.post({
@@ -1888,7 +1888,7 @@ describe('Parse.User testing', () => {
}); });
}); });
it('should fail linking with existing', (done) => { it('should fail linking with existing through REST', (done) => {
const provider = getMockFacebookProvider(); const provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider); Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("facebook", { Parse.User._logInWith("facebook", {

View File

@@ -12,7 +12,7 @@ const dropTable = (client, className) => {
describe_only_db('postgres')('PostgresStorageAdapter', () => { describe_only_db('postgres')('PostgresStorageAdapter', () => {
const adapter = new PostgresStorageAdapter({ uri: databaseURI }) const adapter = new PostgresStorageAdapter({ uri: databaseURI })
beforeEach(() => { beforeEach(() => {
return adapter.dropDatabase(); return adapter.deleteAllClasses();
}); });
it('schemaUpgrade, upgrade the database schema when schema changes', done => { it('schemaUpgrade, upgrade the database schema when schema changes', done => {

View File

@@ -172,7 +172,7 @@ beforeEach(done => {
throw error; throw error;
} }
} }
TestUtils.destroyAllDataPermanently() TestUtils.destroyAllDataPermanently(true)
.catch(error => { .catch(error => {
// For tests that connect to their own mongo, there won't be any data to delete. // For tests that connect to their own mongo, there won't be any data to delete.
if (error.message === 'ns not found' || error.message.startsWith('connect ECONNREFUSED')) { if (error.message === 'ns not found' || error.message.startsWith('connect ECONNREFUSED')) {
@@ -196,7 +196,7 @@ afterEach(function(done) {
fail('There were open connections to the server left after the test finished'); fail('There were open connections to the server left after the test finished');
} }
on_db('postgres', () => { on_db('postgres', () => {
TestUtils.destroyAllDataPermanently().then(done, done); TestUtils.destroyAllDataPermanently(true).then(done, done);
}, done); }, done);
}; };
Parse.Cloud._removeAllHooks(); Parse.Cloud._removeAllHooks();

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,22 @@ const MongoClient = mongodb.MongoClient;
const ReadPreference = mongodb.ReadPreference; const ReadPreference = mongodb.ReadPreference;
const MongoSchemaCollectionName = '_SCHEMA'; const MongoSchemaCollectionName = '_SCHEMA';
const storageAdapterAllCollections = mongoAdapter => {
return mongoAdapter.connect()
.then(() => mongoAdapter.database.collections())
.then(collections => {
return collections.filter(collection => {
if (collection.namespace.match(/\.system\./)) {
return false;
}
// TODO: If you have one app with a collection prefix that happens to be a prefix of another
// apps prefix, this will go very very badly. We should fix that somehow.
return (collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0);
});
});
}
const convertParseSchemaToMongoSchema = ({...schema}) => { const convertParseSchemaToMongoSchema = ({...schema}) => {
delete schema.fields._rperm; delete schema.fields._rperm;
delete schema.fields._wperm; delete schema.fields._wperm;
@@ -297,9 +313,9 @@ export class MongoStorageAdapter implements StorageAdapter {
.catch(err => this.handleError(err)); .catch(err => this.handleError(err));
} }
dropDatabase() { deleteAllClasses(fast: boolean) {
if (!this.database) { return Promise.resolve(); } return storageAdapterAllCollections(this)
return this.database.dropDatabase(); .then(collections => Promise.all(collections.map(collection => fast ? collection.remove({}) : collection.drop())));
} }
// Remove the column and all the data. For Relations, the _Join collection is handled // Remove the column and all the data. For Relations, the _Join collection is handled

View File

@@ -959,10 +959,6 @@ export class PostgresStorageAdapter implements StorageAdapter {
}); });
} }
dropDatabase() {
return this.deleteAllClasses();
}
// Remove the column and all the data. For Relations, the _Join collection is handled // Remove the column and all the data. For Relations, the _Join collection is handled
// specially, this function does not delete _Join columns. It should, however, indicate // specially, this function does not delete _Join columns. It should, however, indicate
// that the relation fields does not exist anymore. In mongo, this means removing it from // that the relation fields does not exist anymore. In mongo, this means removing it from

View File

@@ -31,7 +31,7 @@ export interface StorageAdapter {
createClass(className: string, schema: SchemaType): Promise<void>; createClass(className: string, schema: SchemaType): Promise<void>;
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void>; addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void>;
deleteClass(className: string): Promise<void>; deleteClass(className: string): Promise<void>;
dropDatabase(): Promise<void>; deleteAllClasses(fast: boolean): Promise<void>;
deleteFields(className: string, schema: SchemaType, fieldNames: Array<string>): Promise<void>; deleteFields(className: string, schema: SchemaType, fieldNames: Array<string>): Promise<void>;
getAllClasses(): Promise<StorageClass[]>; getAllClasses(): Promise<StorageClass[]>;
getClass(className: string): Promise<StorageClass>; getClass(className: string): Promise<StorageClass>;

View File

@@ -426,7 +426,7 @@ class DatabaseController {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`); throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
} }
}); });
for (const updateOperation: any in update) { for (const updateOperation in update) {
if (Object.keys(updateOperation).some(innerKey => innerKey.includes('$') || innerKey.includes('.'))) { if (Object.keys(updateOperation).some(innerKey => innerKey.includes('$') || innerKey.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
} }
@@ -654,11 +654,16 @@ class DatabaseController {
} }
// Won't delete collections in the system namespace // Won't delete collections in the system namespace
// Returns a promise. /**
deleteEverything() { * Delete all classes and clears the schema cache
*
* @param {boolean} fast set to true if it's ok to just delete rows and not indexes
* @returns {Promise<void>} when the deletions completes
*/
deleteEverything(fast: boolean = false): Promise<void> {
this.schemaPromise = null; this.schemaPromise = null;
return Promise.all([ return Promise.all([
this.adapter.dropDatabase(), this.adapter.deleteAllClasses(fast),
this.schemaCache.clear() this.schemaCache.clear()
]); ]);
} }

View File

@@ -1,14 +1,17 @@
import AppCache from './cache'; import AppCache from './cache';
//Used by tests /**
export function destroyAllDataPermanently() { * Destroys all data in the database
* @param {boolean} fast set to true if it's ok to just drop objects and not indexes.
*/
export function destroyAllDataPermanently(fast) {
if (!process.env.TESTING) { if (!process.env.TESTING) {
throw 'Only supported in test environment'; throw 'Only supported in test environment';
} }
return Promise.all(Object.keys(AppCache.cache).map(appId => { return Promise.all(Object.keys(AppCache.cache).map(appId => {
const app = AppCache.get(appId); const app = AppCache.get(appId);
if (app.databaseController) { if (app.databaseController) {
return app.databaseController.deleteEverything(); return app.databaseController.deleteEverything(fast);
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }