diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js index 8430e3a660..b7dd5c4538 100644 --- a/lib/all-badge-examples.js +++ b/lib/all-badge-examples.js @@ -301,6 +301,10 @@ const allBadgeExamples = [ title: 'LGTM Alerts', previewUri: '/lgtm/alerts/g/apache/cloudstack.svg' }, + { + title: 'LGTM Grade', + previewUri: '/lgtm/grade/java/g/apache/cloudstack.svg' + }, { title: 'HHVM', previewUri: '/hhvm/symfony/symfony.svg' diff --git a/server.js b/server.js index ab6b4fc1f2..293b45b8f8 100644 --- a/server.js +++ b/server.js @@ -1098,6 +1098,67 @@ cache(function(data, match, sendBadge, request) { }); })); +// LGTM grades integration +camp.route(/^\/lgtm\/grade\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + const language = match[1]; // eg, `java` + const projectId = match[2]; // eg, `g/apache/cloudstack` + const format = match[3]; + const url = 'https://lgtm.com/api/v0.1/project/' + projectId + '/details'; + const languageLabel = (() => { + switch(language) { + case 'cpp': + return 'c/c++'; + case 'csharp': + return 'c#'; + // Javascript analysis on LGTM also includes TypeScript + case 'javascript': + return 'js/ts'; + default: + return language; + } + })(); + const badgeData = getBadgeData('code quality: ' + languageLabel, data); + request(url, function(err, res, buffer) { + if (checkErrorResponse(badgeData, err, res, 'project not found')) { + sendBadge(format, badgeData); + return; + } + try { + const data = JSON.parse(buffer); + if (!('languages' in data)) + throw new Error("Invalid data"); + for (const languageData of data.languages) { + if (languageData.lang === language && 'grade' in languageData) { + // Pretty label for the language + badgeData.text[1] = languageData.grade; + // Pick colour based on grade + if (languageData.grade === 'A+') { + badgeData.colorscheme = 'brightgreen'; + } else if (languageData.grade === 'A') { + badgeData.colorscheme = 'green'; + } else if (languageData.grade === 'B') { + badgeData.colorscheme = 'yellowgreen'; + } else if (languageData.grade === 'C') { + badgeData.colorscheme = 'yellow'; + } else if (languageData.grade === 'D') { + badgeData.colorscheme = 'orange'; + } else { + badgeData.colorscheme = 'red'; + } + sendBadge(format, badgeData); + return; + } + } + badgeData.text[1] = 'no data for language'; + sendBadge(format, badgeData); + } catch(e) { + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + } + }); +})); + // Gratipay integration. camp.route(/^\/(?:gittip|gratipay(\/user|\/team|\/project)?)\/(.*)\.(svg|png|gif|jpg|json)$/, cache(function(queryParams, match, sendBadge, request) { diff --git a/services/lgtm/lgtm.tester.js b/services/lgtm/lgtm.tester.js index 6810e5ba43..1a625596a8 100644 --- a/services/lgtm/lgtm.tester.js +++ b/services/lgtm/lgtm.tester.js @@ -6,56 +6,161 @@ const ServiceTester = require('../service-tester'); const t = new ServiceTester({ id: 'lgtm', title: 'LGTM' }) module.exports = t; -t.create('total alerts for a project') +// Alerts Badge + +t.create('alerts: total alerts for a project') .get('/alerts/g/apache/cloudstack.json') .expectJSONTypes(Joi.object().keys({ name: 'lgtm', value: Joi.string().regex(/^[0-9kM.]+ alerts?$/) })); -t.create('missing project') +t.create('alerts: missing project') .get('/alerts/g/some-org/this-project-doesnt-exist.json') .expectJSON({ name: 'lgtm', value: 'project not found' }); -t.create('no alerts') +t.create('alerts: no alerts') .get('/alerts/g/apache/cloudstack.json') .intercept(nock => nock('https://lgtm.com') .get('/api/v0.1/project/g/apache/cloudstack/details') .reply(200, {alerts: 0})) .expectJSON({ name: 'lgtm', value: '0 alerts' }); -t.create('single alert') +t.create('alerts: single alert') .get('/alerts/g/apache/cloudstack.json') .intercept(nock => nock('https://lgtm.com') .get('/api/v0.1/project/g/apache/cloudstack/details') .reply(200, {alerts: 1})) .expectJSON({ name: 'lgtm', value: '1 alert' }); -t.create('multiple alerts') +t.create('alerts: multiple alerts') .get('/alerts/g/apache/cloudstack.json') .intercept(nock => nock('https://lgtm.com') .get('/api/v0.1/project/g/apache/cloudstack/details') .reply(200, {alerts: 123})) .expectJSON({ name: 'lgtm', value: '123 alerts' }); -t.create('json missing alerts') +t.create('alerts: json missing alerts') .get('/alerts/g/apache/cloudstack.json') .intercept(nock => nock('https://lgtm.com') .get('/api/v0.1/project/g/apache/cloudstack/details') .reply(200, {})) .expectJSON({ name: 'lgtm', value: 'invalid' }); -t.create('invalid json') +t.create('alerts: invalid json') .get('/alerts/g/apache/cloudstack.json') .intercept(nock => nock('https://lgtm.com') .get('/api/v0.1/project/g/apache/cloudstack/details') .reply(200, 'not a json string')) .expectJSON({ name: 'lgtm', value: 'invalid' }); -t.create('lgtm inaccessible') +t.create('alerts: lgtm inaccessible') .get('/alerts/g/apache/cloudstack.json') .networkOff() - .expectJSON({ name: 'lgtm', value: 'inaccessible' }); \ No newline at end of file + .expectJSON({ name: 'lgtm', value: 'inaccessible' }); + +// Grade Badge + +t.create('grade: missing project') + .get('/grade/java/g/some-org/this-project-doesnt-exist.json') + .expectJSON({ + name: 'code quality: java', + value: 'project not found' + }); + +t.create('grade: lgtm inaccessible') + .get('/grade/java/g/apache/cloudstack.json') + .networkOff() + .expectJSON({ name: 'code quality: java', value: 'inaccessible' }); + +t.create('grade: invalid json') + .get('/grade/java/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, 'not a json string')) + .expectJSON({ name: 'code quality: java', value: 'invalid' }); + +t.create('grade: json missing languages') + .get('/grade/java/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, {})) + .expectJSON({ name: 'code quality: java', value: 'invalid' }); + +t.create('grade: grade for a project (java)') + .get('/grade/java/g/apache/cloudstack.json') + .expectJSONTypes(Joi.object().keys({ + name: 'code quality: java', + value: Joi.string().regex(/^(?:A\+)|A|B|C|D|E$/) + })); + +t.create('grade: grade for missing language') + .get('/grade/foo/g/apache/cloudstack.json') + .expectJSON({ + name: 'code quality: foo', + value: 'no data for language' + }); + +// Test display of languages + +const data = {languages: [ + {lang: 'cpp', grade: 'A+'}, + {lang: 'javascript', grade: 'A'}, + {lang: 'java', grade: 'B'}, + {lang: 'python', grade: 'C'}, + {lang: 'csharp', grade: 'D'}, + {lang: 'other', grade: 'E'}, + {lang: 'foo'} +]} + +t.create('grade: cpp') + .get('/grade/cpp/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: c/c++', value: 'A+' }); + +t.create('grade: javascript') + .get('/grade/javascript/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: js/ts', value: 'A' }); + +t.create('grade: java') + .get('/grade/java/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: java', value: 'B' }); + +t.create('grade: python') + .get('/grade/python/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: python', value: 'C' }); + +t.create('grade: csharp') + .get('/grade/csharp/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: c#', value: 'D' }); + +t.create('grade: other') + .get('/grade/other/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: other', value: 'E' }); + +t.create('grade: foo (no grade for valid language)') + .get('/grade/foo/g/apache/cloudstack.json') + .intercept(nock => nock('https://lgtm.com') + .get('/api/v0.1/project/g/apache/cloudstack/details') + .reply(200, data)) + .expectJSON({ name: 'code quality: foo', value: 'no data for language' });