gristlabs_grist-core/test/server/lib/DocApi2.ts
Jarosław Sadziński bd07e9c026 (core) New API to collect timing information from formula evaluation.
Summary:
- /timing/start endpoint to start collecting information
- /timing/stop endpoint to stop collecting
- /timing to retrive data gatherd so far

Timings are collected for all columns (including hidden/helpers/system)

Test Plan: Added new

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D4230
2024-04-24 11:07:11 +02:00

239 lines
9.3 KiB
TypeScript

import {UserAPI} from 'app/common/UserAPI';
import axios from 'axios';
import {assert} from 'chai';
import * as fse from 'fs-extra';
import {TestServer} from 'test/gen-server/apiUtils';
import {configForUser} from 'test/gen-server/testUtils';
import {createTmpDir} from 'test/server/docTools';
import {openClient} from 'test/server/gristClient';
import * as testUtils from 'test/server/testUtils';
const chimpy = configForUser('Chimpy');
const kiwi = configForUser('Kiwi');
describe('DocApi2', function() {
this.timeout(40000);
let server: TestServer;
let homeUrl: string;
let owner: UserAPI;
let wsId: number;
testUtils.setTmpLogLevel('error');
let oldEnv: testUtils.EnvironmentSnapshot;
before(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
const tmpDir = await createTmpDir();
process.env.GRIST_DATA_DIR = tmpDir;
process.env.STRIPE_ENDPOINT_SECRET = 'TEST_WITHOUT_ENDPOINT_SECRET';
// Use the TEST_REDIS_URL as the global redis url, if supplied.
if (process.env.TEST_REDIS_URL && !process.env.REDIS_URL) {
process.env.REDIS_URL = process.env.TEST_REDIS_URL;
}
server = new TestServer(this);
homeUrl = await server.start(['home', 'docs']);
const api = await server.createHomeApi('chimpy', 'docs', true);
await api.newOrg({name: 'testy', domain: 'testy'});
owner = await server.createHomeApi('chimpy', 'testy', true);
wsId = await owner.newWorkspace({name: 'ws'}, 'current');
});
after(async function() {
const api = await server.createHomeApi('chimpy', 'docs');
await api.deleteOrg('testy');
await server.stop();
oldEnv.restore();
});
describe('DELETE /docs/{did}', async () => {
it('permanently deletes a document and all of its forks', async function() {
// Create a new document and fork it twice.
const docId = await owner.newDoc({name: 'doc'}, wsId);
const session = await owner.getSessionActive();
const client = await openClient(server.server, session.user.email, session.org?.domain || 'docs');
await client.openDocOnConnect(docId);
const forkDocResponse1 = await client.send('fork', 0);
const forkDocResponse2 = await client.send('fork', 0);
// Check that files were created for the trunk and forks.
const docPath = server.server.getStorageManager().getPath(docId);
const forkPath1 = server.server.getStorageManager().getPath(forkDocResponse1.data.docId);
const forkPath2 = server.server.getStorageManager().getPath(forkDocResponse2.data.docId);
assert.equal(await fse.pathExists(docPath), true);
assert.equal(await fse.pathExists(forkPath1), true);
assert.equal(await fse.pathExists(forkPath2), true);
// Delete the trunk via API.
const deleteDocResponse = await axios.delete(`${homeUrl}/api/docs/${docId}`, chimpy);
assert.equal(deleteDocResponse.status, 200);
// Check that files for the trunk and forks were deleted.
assert.equal(await fse.pathExists(docPath), false);
assert.equal(await fse.pathExists(forkPath1), false);
assert.equal(await fse.pathExists(forkPath2), false);
});
});
describe('/docs/{did}/timing', async () => {
let docId: string;
before(async function() {
docId = await owner.newDoc({name: 'doc2'}, wsId);
});
after(async function() {
await owner.deleteDoc(docId);
});
// There are two endpoints here /timing/start and /timing/stop.
// Here we just test that it is operational, available only for owners
// and that it returns sane results. Exact tests are done in python.
// Smoke test.
it('POST /docs/{did}/timing smoke tests', async function() {
// We are disabled.
let resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {status: 'disabled'});
// Start it.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, chimpy);
assert.equal(resp.status, 200);
// Stop it.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, []);
});
it('POST /docs/{did}/timing/start', async function() {
// Start timing as non owner, should fail.
let resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, kiwi);
assert.equal(resp.status, 403);
// Query status as non owner, should fail.
resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, kiwi);
assert.equal(resp.status, 403);
// Check as owner.
resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {status: 'disabled'});
// Start timing as owner.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, chimpy);
assert.equal(resp.status, 200);
// Check we are started.
resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {status: 'active', timing: []});
// Starting timing again works as expected, returns 400 as this is already.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, chimpy);
assert.equal(resp.status, 400);
// As non owner
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, kiwi);
assert.equal(resp.status, 403);
});
it('POST /docs/{did}/timing/stop', async function() {
// Timings are turned on, so we can stop them.
// First as non owner, we should fail.
let resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, kiwi);
assert.equal(resp.status, 403);
// Next as owner.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, chimpy);
assert.equal(resp.status, 200);
// Now do it once again, we should got 400, as we are not timing.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, chimpy);
assert.equal(resp.status, 400);
});
it('GET /docs/{did}/timing', async function() {
// Now we can check the results. Start timing and check that we got [] in response.
let resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, chimpy);
assert.equal(resp.status, 200);
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, []);
// Now create a table with a formula column and make sure we see it in the results.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/apply`, [
['AddTable', 'Timings', [
{id: 'A', formula: '$id' }
]],
], chimpy);
assert.equal(resp.status, 200);
// Now start it again,
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, chimpy);
assert.equal(resp.status, 200);
// Make sure we see that it is active and we have some intermediate results
resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {status: 'active', timing: []});
// And trigger some formula calculations.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/apply`, [
['BulkAddRecord', 'Timings', [null, null], {}],
], chimpy);
assert.equal(resp.status, 200, JSON.stringify(resp.data));
// Make sure we can't stop it as non owner.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, kiwi);
assert.equal(resp.status, 403);
// Now stop it as owner and make sure the result is sane.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/stop`, {}, chimpy);
assert.equal(resp.status, 200, JSON.stringify(resp.data));
const data = resp.data as Array<{
tableId: string;
colId: string;
sum: number;
count: number;
average: number;
max: number;
markers?: Array<{
name: string;
sum: number;
count: number;
average: number;
max: number;
}>
}>;
assert.isAbove(data.length, 0);
assert.equal(data[0].tableId, 'Timings');
assert.isTrue(typeof data[0].sum === 'number');
assert.isTrue(typeof data[0].count === 'number');
assert.isTrue(typeof data[0].average === 'number');
assert.isTrue(typeof data[0].max === 'number');
});
it('POST /docs/{did}/timing/start remembers state after reload', async function() {
// Make sure we are off.
let resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
assert.equal(resp.status, 200);
assert.deepEqual(resp.data, {status: 'disabled'});
// Now start it.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/timing/start`, {}, chimpy);
assert.equal(resp.status, 200);
// Now reload document.
resp = await axios.post(`${homeUrl}/api/docs/${docId}/force-reload`, {}, chimpy);
assert.equal(resp.status, 200);
// And check that we are still on.
resp = await axios.get(`${homeUrl}/api/docs/${docId}/timing`, chimpy);
assert.equal(resp.status, 200, JSON.stringify(resp.data));
assert.deepEqual(resp.data, {status: 'active', timing: []});
});
});
});