Skip to content

Commit

Permalink
feat(server): add route for computing build ancestor
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce committed Oct 3, 2019
1 parent 7c0d769 commit e19d9cd
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 8 deletions.
14 changes: 14 additions & 0 deletions packages/server/src/api/routes/projects.js
Expand Up @@ -117,6 +117,20 @@ function createRouter(context) {
})
);

// GET /projects/:id/builds/:id/ancestor
router.get(
'/:projectId/builds/:buildId/ancestor',
handleAsyncError(async (req, res) => {
const build = await context.storageMethod.findAncestorBuildById(
req.params.projectId,
req.params.buildId
);

if (!build) return res.sendStatus(404);
res.json(build);
})
);

// GET /projects/<id>/builds/<id>/runs
router.get(
'/:projectId/builds/:buildId/runs',
Expand Down
46 changes: 46 additions & 0 deletions packages/server/src/api/storage/sql/sql.js
Expand Up @@ -303,6 +303,52 @@ class SqlStorageMethod {
return clone(build || undefined);
}

/**
* @param {string} projectId
* @param {string} buildId
* @return {Promise<LHCI.ServerCommand.Build | undefined>}
*/
async findAncestorBuildById(projectId, buildId) {
const {buildModel} = this._sql();
const build = await this._findByPk(buildModel, buildId);
if (!build || (build && build.projectId !== projectId)) return undefined;

if (build.ancestorHash) {
const ancestorsByHash = await this._findAll(buildModel, {
where: {projectId: build.projectId, hash: build.ancestorHash},
limit: 1,
});

if (ancestorsByHash.length) return clone(ancestorsByHash[0]);
}

const where = {
projectId: build.projectId,
branch: 'master',
id: {[Sequelize.Op.ne]: build.id},
};

const nearestBuildAfter = await buildModel.findAll({
where: {...where, runAt: {[Sequelize.Op.gte]: build.runAt}},
order: [['runAt', 'ASC']],
limit: 1,
});

const nearestBuildBefore = await buildModel.findAll({
where: {...where, runAt: {[Sequelize.Op.lte]: build.runAt}},
order: [['runAt', 'DESC']],
limit: 1,
});

/** @param {string} date */
const getDateDistance = date =>
Math.abs(new Date(date).getTime() - new Date(build.runAt).getTime());
const candidates = nearestBuildBefore
.concat(nearestBuildAfter)
.sort((a, b) => getDateDistance(a.runAt) - getDateDistance(b.runAt));
return clone(candidates[0]);
}

/**
* @param {string} projectId
* @param {string} buildId
Expand Down
10 changes: 10 additions & 0 deletions packages/server/src/api/storage/storage-method.js
Expand Up @@ -96,6 +96,16 @@ class StorageMethod {
throw new Error('Unimplemented');
}

/**
* @param {string} projectId
* @param {string} buildId
* @return {Promise<LHCI.ServerCommand.Build | undefined>}
*/
// eslint-disable-next-line no-unused-vars
async findAncestorBuildById(projectId, buildId) {
throw new Error('Unimplemented');
}

/**
* @param {StrictOmit<LHCI.ServerCommand.Build, 'id'>} unsavedBuild
* @return {Promise<LHCI.ServerCommand.Build>}
Expand Down
90 changes: 82 additions & 8 deletions packages/server/test/server-test-suite.js
Expand Up @@ -18,6 +18,7 @@ function runTests(state) {
let buildA;
let buildB;
let buildC;
let buildD;
let runA;
let runB;
let runC;
Expand Down Expand Up @@ -103,7 +104,7 @@ function runTests(state) {
author: 'Paul Irish <paul@example.com>',
avatarUrl: 'https://avatars1.githubusercontent.com/u/39191?s=460&v=4',
commitMessage: 'feat: add some more awesome features',
ancestorHash: 'e0acdd50ed0fdcfdceb2508498be50cc55c696ef',
ancestorHash: buildA.hash,
runAt: new Date().toISOString(),
};

Expand All @@ -113,6 +114,26 @@ function runTests(state) {
expect(buildB).toMatchObject(payload);
});

it('should create a 3rd build (w/o an ancestor)', async () => {
const payload = {
projectId: projectA.id,
lifecycle: 'unsealed',
hash: '50cc55c696eb25084ebe0acdd50ed0fdcfdce98a',
branch: 'other_branch',
externalBuildUrl: 'http://travis-ci.org/org/repo/2',
author: 'Paul Irish <paul@example.com>',
avatarUrl: 'https://avatars1.githubusercontent.com/u/39191?s=460&v=4',
commitMessage: 'feat: a branch without an ancestor',
ancestorHash: '',
runAt: new Date().toISOString(),
};

buildC = await client.createBuild(payload);
expect(buildC).toHaveProperty('id');
expect(buildC.projectId).toEqual(projectA.id);
expect(buildC).toMatchObject(payload);
});

it('should create a build in different project', async () => {
const payload = {
projectId: projectB.id,
Expand All @@ -127,15 +148,15 @@ function runTests(state) {
runAt: new Date().toISOString(),
};

buildC = await client.createBuild(payload);
expect(buildC).toHaveProperty('id');
expect(buildC.projectId).toEqual(projectB.id);
expect(buildC).toMatchObject(payload);
buildD = await client.createBuild(payload);
expect(buildD).toHaveProperty('id');
expect(buildD.projectId).toEqual(projectB.id);
expect(buildD).toMatchObject(payload);
});

it('should list builds', async () => {
const builds = await client.getBuilds(projectA.id);
expect(builds).toEqual([buildB, buildA]);
expect(builds).toEqual([buildC, buildB, buildA]);
});

it('should list builds filtered by branch', async () => {
Expand All @@ -150,7 +171,7 @@ function runTests(state) {

it('should list builds for another project', async () => {
const builds = await client.getBuilds(projectB.id);
expect(builds).toEqual([buildC]);
expect(builds).toEqual([buildD]);
});

it('should list builds for missing project', async () => {
Expand All @@ -172,7 +193,11 @@ function runTests(state) {
describe('/:projectId/branches', () => {
it('should list branches', async () => {
const branches = await client.getBranches(projectA.id);
expect(branches).toEqual([{branch: 'test_branch'}, {branch: 'master'}]);
expect(branches).toEqual([
{branch: 'test_branch'},
{branch: 'other_branch'},
{branch: 'master'},
]);
});

it('should handle missing ids', async () => {
Expand All @@ -181,6 +206,55 @@ function runTests(state) {
});
});

describe('/:projectId/builds/:buildId/ancestor', () => {
it('should not find a build with no ancestor', async () => {
const build = await client.findAncestorBuildById(buildA.projectId, buildA.id);
expect(build).toEqual(undefined);
});

it('should find a build with an explicit ancestor', async () => {
const build = await client.findAncestorBuildById(buildB.projectId, buildB.id);
expect(build).toEqual(buildA);
});

it('should find a build with an implicit prior ancestor', async () => {
const build = await client.findAncestorBuildById(buildC.projectId, buildC.id);
expect(build).toEqual(buildA);
});

it('should find a build with complicated ancestor', async () => {
const project = await client.createProject(projectA);
const buildWithoutAncestor = await client.createBuild({
...buildC,
projectId: project.id,
branch: 'feature_branch',
runAt: new Date('2019-09-01').toISOString(),
});

let ancestor = await client.findAncestorBuildById(project.id, buildWithoutAncestor.id);
expect(ancestor).toEqual(undefined);

const implicitPriorAncestorBuild = await client.createBuild({
...buildA,
projectId: project.id,
branch: 'master',
runAt: new Date('2019-01-01').toISOString(),
});
ancestor = await client.findAncestorBuildById(project.id, buildWithoutAncestor.id);
expect(ancestor).toEqual(implicitPriorAncestorBuild);

const implicitFutureAncestorBuild = await client.createBuild({
...buildA,
projectId: project.id,
branch: 'master',
runAt: new Date('2019-09-02').toISOString(),
});

ancestor = await client.findAncestorBuildById(project.id, buildWithoutAncestor.id);
expect(ancestor).toEqual(implicitFutureAncestorBuild);
});
});

describe('/:projectId/builds/:buildId/runs', () => {
const lhr = {
lighthouseVersion: '4.1.0',
Expand Down
11 changes: 11 additions & 0 deletions packages/utils/src/api-client.js
Expand Up @@ -205,6 +205,17 @@ class ApiClient {
return this._convert404ToUndefined(this._get(`/v1/projects/${projectId}/builds/${buildId}`));
}

/**
* @param {string} projectId
* @param {string} buildId
* @return {Promise<LHCI.ServerCommand.Build | undefined>}
*/
async findAncestorBuildById(projectId, buildId) {
return this._convert404ToUndefined(
this._get(`/v1/projects/${projectId}/builds/${buildId}/ancestor`)
);
}

/**
* @param {string} projectId
* @param {string} buildId
Expand Down

0 comments on commit e19d9cd

Please sign in to comment.