Move "good" badge helpers from lib/ to services/ (#3101)

This moves a few helpers from `lib/` to `services/`:

build-status.js
build-status.spec.js
color-formatters.js
color-formatters.spec.js
contributor-count.js
licenses.js
licenses.spec.js
php-version.js
php-version.spec.js
text-formatters.js
text-formatters.spec.js
version.js
version.spec.js

And one from `lib/` to `core/`:

unhandled-rejection.spec.js

The diff is long, but the changes are straightforward.

Ref #2832
This commit is contained in:
Paul Melnikow
2019-02-27 20:47:46 -05:00
committed by GitHub
parent 8b2ecaefd1
commit fafb22efee
178 changed files with 326 additions and 354 deletions

View File

@@ -1,66 +0,0 @@
'use strict'
const Joi = require('joi')
const greenStatuses = [
'fixed',
'passed',
'passing',
'succeeded',
'success',
'successful',
]
const orangeStatuses = ['partially succeeded', 'unstable', 'timeout']
const redStatuses = ['error', 'failed', 'failing']
const otherStatuses = [
'building',
'canceled',
'cancelled',
'expired',
'no tests',
'not built',
'not run',
'pending',
'processing',
'queued',
'running',
'scheduled',
'skipped',
'starting',
'stopped',
'waiting',
]
const isBuildStatus = Joi.equal(
greenStatuses
.concat(orangeStatuses)
.concat(redStatuses)
.concat(otherStatuses)
)
function renderBuildStatusBadge({ label, status }) {
let message
let color
if (greenStatuses.includes(status)) {
message = 'passing'
color = 'brightgreen'
} else if (orangeStatuses.includes(status)) {
message = status === 'partially succeeded' ? 'passing' : status
color = 'orange'
} else if (redStatuses.includes(status)) {
message = status === 'failed' ? 'failing' : status
color = 'red'
} else {
message = status
}
return {
label,
message,
color,
}
}
module.exports = { isBuildStatus, renderBuildStatusBadge }

View File

@@ -1,85 +0,0 @@
'use strict'
const { expect } = require('chai')
const { test, given, forCases } = require('sazerac')
const { renderBuildStatusBadge } = require('./build-status')
test(renderBuildStatusBadge, () => {
given({ label: 'build', status: 'passed' }).expect({
label: 'build',
message: 'passing',
color: 'brightgreen',
})
given({ label: 'build', status: 'success' }).expect({
label: 'build',
message: 'passing',
color: 'brightgreen',
})
given({ label: 'build', status: 'partially succeeded' }).expect({
label: 'build',
message: 'passing',
color: 'orange',
})
given({ label: 'build', status: 'failed' }).expect({
label: 'build',
message: 'failing',
color: 'red',
})
given({ label: 'build', status: 'error' }).expect({
label: 'build',
message: 'error',
color: 'red',
})
})
test(renderBuildStatusBadge, () => {
forCases([
given({ status: 'fixed' }),
given({ status: 'passed' }),
given({ status: 'passing' }),
given({ status: 'succeeded' }),
given({ status: 'success' }),
given({ status: 'successful' }),
]).assert('should be brightgreen', b =>
expect(b).to.include({ color: 'brightgreen' })
)
})
test(renderBuildStatusBadge, () => {
forCases([
given({ status: 'partially succeeded' }),
given({ status: 'timeout' }),
given({ status: 'unstable' }),
]).assert('should be orange', b => expect(b).to.include({ color: 'orange' }))
})
test(renderBuildStatusBadge, () => {
forCases([
given({ status: 'error' }),
given({ status: 'failed' }),
given({ status: 'failing' }),
]).assert('should be red', b => expect(b).to.include({ color: 'red' }))
})
test(renderBuildStatusBadge, () => {
forCases([
given({ status: 'building' }),
given({ status: 'canceled' }),
given({ status: 'cancelled' }),
given({ status: 'expired' }),
given({ status: 'no tests' }),
given({ status: 'not built' }),
given({ status: 'not run' }),
given({ status: 'pending' }),
given({ status: 'processing' }),
given({ status: 'queued' }),
given({ status: 'running' }),
given({ status: 'scheduled' }),
given({ status: 'skipped' }),
given({ status: 'starting' }),
given({ status: 'stopped' }),
given({ status: 'waiting' }),
]).assert('should have undefined color', b =>
expect(b).to.include({ color: undefined })
)
})

View File

@@ -1,117 +0,0 @@
/**
* Commonly-used functions for determining the colour to use for a badge,
* including colours based off download count, version number, etc.
*/
'use strict'
const moment = require('moment')
function version(version) {
if (typeof version !== 'string' && typeof version !== 'number') {
throw new Error(`Can't generate a version color for ${version}`)
}
version = `${version}`
let first = version[0]
if (first === 'v') {
first = version[1]
}
if (first === '0' || /alpha|beta|snapshot|dev|pre/i.test(version)) {
return 'orange'
} else {
return 'blue'
}
}
function downloadCount(downloads) {
return floorCount(downloads, 10, 100, 1000)
}
function coveragePercentage(percentage) {
return floorCount(percentage, 80, 90, 100)
}
function floorCount(value, yellow, yellowgreen, green) {
if (value <= 0) {
return 'red'
} else if (value < yellow) {
return 'yellow'
} else if (value < yellowgreen) {
return 'yellowgreen'
} else if (value < green) {
return 'green'
} else {
return 'brightgreen'
}
}
function letterScore(score) {
if (score === 'A') {
return 'brightgreen'
} else if (score === 'B') {
return 'green'
} else if (score === 'C') {
return 'yellowgreen'
} else if (score === 'D') {
return 'yellow'
} else if (score === 'E') {
return 'orange'
} else {
return 'red'
}
}
function colorScale(steps, colors, reversed) {
if (steps === undefined) {
throw Error('When invoking colorScale, steps should be provided.')
}
const defaultColors = {
1: ['red', 'brightgreen'],
2: ['red', 'yellow', 'brightgreen'],
3: ['red', 'yellow', 'green', 'brightgreen'],
4: ['red', 'yellow', 'yellowgreen', 'green', 'brightgreen'],
5: ['red', 'orange', 'yellow', 'yellowgreen', 'green', 'brightgreen'],
}
if (typeof colors === 'undefined') {
if (steps.length in defaultColors) {
colors = defaultColors[steps.length]
} else {
throw Error(`No default colors for ${steps.length} steps.`)
}
}
if (steps.length !== colors.length - 1) {
throw Error(
'When colors are provided, there should be n + 1 colors for n steps.'
)
}
if (reversed) {
colors = Array.from(colors).reverse()
}
return value => {
const stepIndex = steps.findIndex(step => value < step)
// For the final step, stepIndex is -1, so in all cases this expression
// works swimmingly.
return colors.slice(stepIndex)[0]
}
}
function age(date) {
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, true)
const daysElapsed = moment().diff(moment(date), 'days')
return colorByAge(daysElapsed)
}
module.exports = {
version,
downloadCount,
coveragePercentage,
floorCount,
letterScore,
colorScale,
age,
}

View File

@@ -1,111 +0,0 @@
'use strict'
const { test, given, forCases } = require('sazerac')
const { expect } = require('chai')
const {
coveragePercentage,
colorScale,
letterScore,
age,
version,
} = require('./color-formatters')
describe('Color formatters', function() {
const byPercentage = colorScale([Number.EPSILON, 80, 90, 100])
test(byPercentage, () => {
given(-1).expect('red')
given(0).expect('red')
given(0.5).expect('yellow')
given(1).expect('yellow')
given(50).expect('yellow')
given(80).expect('yellowgreen')
given(85).expect('yellowgreen')
given(90).expect('green')
given(100).expect('brightgreen')
given(101).expect('brightgreen')
forCases(
[-1, 0, 0.5, 1, 50, 80, 85, 90, 100, 101].map(v =>
given(v).expect(coveragePercentage(v))
)
).should("return '%s', for parity with coveragePercentage()")
})
context('when reversed', function() {
test(colorScale([7, 30, 180, 365, 730], undefined, true), () => {
given(3).expect('brightgreen')
given(7).expect('green')
given(10).expect('green')
given(60).expect('yellowgreen')
given(250).expect('yellow')
given(400).expect('orange')
given(800).expect('red')
})
})
test(letterScore, () => {
given('A').expect('brightgreen')
given('B').expect('green')
given('C').expect('yellowgreen')
given('D').expect('yellow')
given('E').expect('orange')
given('F').expect('red')
given('Z').expect('red')
})
const monthsAgo = months => {
const result = new Date()
// This looks wack but it works.
result.setMonth(result.getMonth() - months)
return result
}
test(age, () => {
given(Date.now())
.describe('when given the current timestamp')
.expect('brightgreen')
given(new Date())
.describe('when given the current Date')
.expect('brightgreen')
given(new Date(2001, 1, 1))
.describe('when given a Date many years ago')
.expect('red')
given(monthsAgo(2))
.describe('when given a Date two months ago')
.expect('yellowgreen')
given(monthsAgo(15))
.describe('when given a Date 15 months ago')
.expect('orange')
})
test(version, () => {
forCases([given('1.0'), given(9), given(1.0)]).expect('blue')
forCases([
given(0.1),
given('0.9'),
given('1.0-Beta'),
given('1.1-alpha'),
given('6.0-SNAPSHOT'),
given('1.0.1-dev'),
given('2.1.6-prerelease'),
]).expect('orange')
expect(() => version(null)).to.throw(
Error,
"Can't generate a version color for null"
)
expect(() => version(undefined)).to.throw(
Error,
"Can't generate a version color for undefined"
)
expect(() => version(true)).to.throw(
Error,
"Can't generate a version color for true"
)
expect(() => version({})).to.throw(
Error,
"Can't generate a version color for [object Object]"
)
})
})

View File

@@ -1,26 +0,0 @@
'use strict'
const { metric } = require('./text-formatters')
function contributorColor(contributorCount) {
if (contributorCount > 2) {
return 'brightgreen'
} else if (contributorCount === 2) {
return 'yellow'
} else {
return 'red'
}
}
function renderContributorBadge({ label, contributorCount }) {
return {
label,
message: metric(contributorCount),
color: contributorColor(contributorCount),
}
}
module.exports = {
contributorColor,
renderContributorBadge,
}

View File

@@ -1,97 +0,0 @@
'use strict'
const { toArray } = require('./badge-data')
const licenseTypes = {
// permissive licenses - not public domain and not copyleft
permissive: {
spdxLicenseIds: [
'AFL-3.0',
'Apache-2.0',
'Artistic-2.0',
'BSD-2-Clause',
'BSD-3-Clause',
'BSD-3-Clause-Clear',
'BSL-1.0',
'CC-BY-4.0',
'ECL-2.0',
'ISC',
'MIT',
'MS-PL',
'NCSA',
'PostgreSQL',
'Zlib',
],
color: 'green',
priority: '2',
},
// copyleft licenses require 'Disclose source' (https://choosealicense.com/appendix/#disclose-source)
// or 'Same license' (https://choosealicense.com/appendix/#same-license)
copyleft: {
spdxLicenseIds: [
'AGPL-3.0',
'CC-BY-SA-4.0',
'EPL-1.0',
'EUPL-1.1',
'GPL-2.0',
'GPL-3.0',
'LGPL-2.1',
'LGPL-3.0',
'LPPL-1.3c',
'MPL-2.0',
'MS-RL',
'OFL-1.1',
'OSL-3.0',
],
color: 'orange',
priority: '1',
},
// public domain licenses do not require 'License and copyright notice' (https://choosealicense.com/appendix/#include-copyright)
'public-domain': {
spdxLicenseIds: ['CC0-1.0', 'Unlicense', 'WTFPL'],
color: '7cd958',
priority: '3',
},
}
const licenseToColorMap = {}
Object.keys(licenseTypes).forEach(licenseType => {
const { spdxLicenseIds, color, priority } = licenseTypes[licenseType]
spdxLicenseIds.forEach(license => {
licenseToColorMap[license] = { color, priority }
})
})
const defaultLicenseColor = 'lightgrey'
const licenseToColor = spdxId => {
if (!Array.isArray(spdxId)) {
return (
(licenseToColorMap[spdxId] && licenseToColorMap[spdxId].color) ||
defaultLicenseColor
)
}
const licenseType = spdxId
.filter(i => licenseToColorMap[i])
.map(i => licenseToColorMap[i])
.reduce((a, b) => (a.priority > b.priority ? a : b), {
color: defaultLicenseColor,
priority: 0,
})
return licenseType.color
}
function renderLicenseBadge({ license, licenses }) {
if (licenses === undefined) {
licenses = toArray(license)
}
if (licenses.length === 0) {
return { message: 'missing', color: 'red' }
}
return {
message: licenses.join(', '),
color: licenseToColor(licenses),
}
}
module.exports = { licenseToColor, renderLicenseBadge }

View File

@@ -1,46 +0,0 @@
'use strict'
const { test, given, forCases } = require('sazerac')
const { licenseToColor, renderLicenseBadge } = require('./licenses')
describe('license helpers', function() {
test(licenseToColor, () => {
given('MIT').expect('green')
given('MPL-2.0').expect('orange')
given('Unlicense').expect('7cd958')
given('unknown-license').expect('lightgrey')
given(null).expect('lightgrey')
given(['CC0-1.0', 'MPL-2.0']).expect('7cd958')
given(['MPL-2.0', 'CC0-1.0']).expect('7cd958')
given(['MIT', 'MPL-2.0']).expect('green')
given(['MPL-2.0', 'MIT']).expect('green')
given(['OFL-1.1', 'MPL-2.0']).expect('orange')
given(['MPL-2.0', 'OFL-1.1']).expect('orange')
given(['CC0-1.0', 'MIT', 'MPL-2.0']).expect('7cd958')
given(['UNKNOWN-1.0', 'MIT']).expect('green')
given(['UNKNOWN-1.0', 'UNKNOWN-2.0']).expect('lightgrey')
})
test(renderLicenseBadge, () => {
forCases([
given({ license: undefined }),
given({ licenses: [] }),
given({}),
]).expect({
message: 'missing',
color: 'red',
})
forCases([
given({ license: 'WTFPL' }),
given({ licenses: ['WTFPL'] }),
]).expect({
message: 'WTFPL',
color: '7cd958',
})
given({ licenses: ['MPL-2.0', 'MIT'] }).expect({
message: 'MPL-2.0, MIT',
color: 'green',
})
})
})

View File

@@ -1,247 +0,0 @@
/**
* Utilities relating to PHP version numbers. This compares version numbers
* using the algorithm followed by Composer (see
* https://getcomposer.org/doc/04-schema.md#version).
*/
'use strict'
const { promisify } = require('util')
const request = require('request')
const { listCompare } = require('./version')
const { omitv } = require('./text-formatters')
const { regularUpdate } = require('./regular-update')
// Return a negative value if v1 < v2,
// zero if v1 = v2, a positive value otherwise.
function asciiVersionCompare(v1, v2) {
if (v1 < v2) {
return -1
} else if (v1 > v2) {
return 1
} else {
return 0
}
}
// Take a version without the starting v.
// eg, '1.0.x-beta'
// Return { numbers: [1,0,something big], modifier: 2, modifierCount: 1 }
function numberedVersionData(version) {
// A version has a numbered part and a modifier part
// (eg, 1.0.0-patch, 2.0.x-dev).
const parts = version.split('-')
const numbered = parts[0]
// Aliases that get caught here.
if (numbered === 'dev') {
return {
numbers: parts[1],
modifier: 5,
modifierCount: 1,
}
}
let modifierLevel = 3
let modifierLevelCount = 0
if (parts.length > 1) {
const modifier = parts[parts.length - 1]
const firstLetter = modifier.charCodeAt(0)
let modifierLevelCountString
// Modifiers: alpha < beta < RC < normal < patch < dev
if (firstLetter === 97) {
// a
modifierLevel = 0
if (/^alpha/.test(modifier)) {
modifierLevelCountString = +modifier.slice(5)
} else {
modifierLevelCountString = +modifier.slice(1)
}
} else if (firstLetter === 98) {
// b
modifierLevel = 1
if (/^beta/.test(modifier)) {
modifierLevelCountString = +modifier.slice(4)
} else {
modifierLevelCountString = +modifier.slice(1)
}
} else if (firstLetter === 82) {
// R
modifierLevel = 2
modifierLevelCountString = +modifier.slice(2)
} else if (firstLetter === 112) {
// p
modifierLevel = 4
if (/^patch/.test(modifier)) {
modifierLevelCountString = +modifier.slice(5)
} else {
modifierLevelCountString = +modifier.slice(1)
}
} else if (firstLetter === 100) {
// d
modifierLevel = 5
if (/^dev/.test(modifier)) {
modifierLevelCountString = +modifier.slice(3)
} else {
modifierLevelCountString = +modifier.slice(1)
}
}
// If we got the empty string, it defaults to a modifier count of 1.
if (!modifierLevelCountString) {
modifierLevelCount = 1
} else {
modifierLevelCount = +modifierLevelCountString
}
}
// Try to convert to a list of numbers.
function toNum(s) {
let n = +s
if (Number.isNaN(n)) {
n = 0xffffffff
}
return n
}
const numberList = numbered.split('.').map(toNum)
return {
numbers: numberList,
modifier: modifierLevel,
modifierCount: modifierLevelCount,
}
}
// Return a negative value if v1 < v2,
// zero if v1 = v2,
// a positive value otherwise.
//
// See https://getcomposer.org/doc/04-schema.md#version
// and https://github.com/badges/shields/issues/319#issuecomment-74411045
function compare(v1, v2) {
// Omit the starting `v`.
const rawv1 = omitv(v1)
const rawv2 = omitv(v2)
let v1data, v2data
try {
v1data = numberedVersionData(rawv1)
v2data = numberedVersionData(rawv2)
} catch (e) {
return asciiVersionCompare(rawv1, rawv2)
}
// Compare the numbered part (eg, 1.0.0 < 2.0.0).
const numbersCompare = listCompare(v1data.numbers, v2data.numbers)
if (numbersCompare !== 0) {
return numbersCompare
}
// Compare the modifiers (eg, alpha < beta).
if (v1data.modifier < v2data.modifier) {
return -1
} else if (v1data.modifier > v2data.modifier) {
return 1
}
// Compare the modifier counts (eg, alpha1 < alpha3).
if (v1data.modifierCount < v2data.modifierCount) {
return -1
} else if (v1data.modifierCount > v2data.modifierCount) {
return 1
}
return 0
}
function latest(versions) {
let latest = versions[0]
for (let i = 1; i < versions.length; i++) {
if (compare(latest, versions[i]) < 0) {
latest = versions[i]
}
}
return latest
}
function isStable(version) {
const rawVersion = omitv(version)
let versionData
try {
versionData = numberedVersionData(rawVersion)
} catch (e) {
return false
}
// normal or patch
return versionData.modifier === 3 || versionData.modifier === 4
}
function minorVersion(version) {
const result = version.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
if (result === null) {
return ''
}
return `${result[1]}.${result[2] ? result[2] : '0'}`
}
function versionReduction(versions, phpReleases) {
if (!versions.length) {
return ''
}
// versions intersect
versions = Array.from(new Set(versions))
.filter(n => phpReleases.includes(n))
.sort()
// nothing to reduction
if (versions.length < 2) {
return versions.length ? versions[0] : ''
}
const first = phpReleases.indexOf(versions[0])
const last = phpReleases.indexOf(versions[versions.length - 1])
// no missed versions
if (first + versions.length - 1 === last) {
if (last === phpReleases.length - 1) {
return `>= ${versions[0][2] === '0' ? versions[0][0] : versions[0]}` // 7.0 -> 7
}
return `${versions[0]} - ${versions[versions.length - 1]}`
}
return versions.join(', ')
}
function getPhpReleases(githubApiProvider) {
return promisify(regularUpdate)({
url: '/repos/php/php-src/git/refs/tags',
intervalMillis: 24 * 3600 * 1000, // 1 day
scraper: tags =>
Array.from(
new Set(
tags
// only releases
.filter(
tag => tag.ref.match(/^refs\/tags\/php-\d+\.\d+\.\d+$/) != null
)
// get minor version of release
.map(tag => tag.ref.match(/^refs\/tags\/php-(\d+\.\d+)\.\d+$/)[1])
)
),
request: (url, options, cb) =>
githubApiProvider.request(request, url, {}, cb),
})
}
module.exports = {
compare,
latest,
isStable,
minorVersion,
versionReduction,
getPhpReleases,
}

View File

@@ -1,76 +0,0 @@
'use strict'
const { test, given } = require('sazerac')
const { compare, minorVersion, versionReduction } = require('./php-version')
const phpReleases = [
'5.0',
'5.1',
'5.2',
'5.3',
'5.4',
'5.5',
'5.6',
'7.0',
'7.1',
'7.2',
]
describe('Text PHP version', function() {
test(minorVersion, () => {
given('7').expect('7.0')
given('7.1').expect('7.1')
given('5.3.3').expect('5.3')
given('hhvm').expect('')
})
test(versionReduction, () => {
given(['5.3', '5.4', '5.5'], phpReleases).expect('5.3 - 5.5')
given(['5.4', '5.5', '5.6', '7.0', '7.1'], phpReleases).expect('5.4 - 7.1')
given(['5.5', '5.6', '7.0', '7.1', '7.2'], phpReleases).expect('>= 5.5')
given(['5.5', '5.6', '7.1', '7.2'], phpReleases).expect(
'5.5, 5.6, 7.1, 7.2'
)
given(['7.0', '7.1', '7.2'], phpReleases).expect('>= 7')
given(
['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '7.0', '7.1', '7.2'],
phpReleases
).expect('>= 5')
given(['7.1', '7.2'], phpReleases).expect('>= 7.1')
given(['7.1'], phpReleases).expect('7.1')
given(['8.1'], phpReleases).expect('')
given([]).expect('')
})
})
describe('Composer version comparison', function() {
test(compare, () => {
// composer version scheme ordering
given('0.9.0', '1.0.0-alpha').expect(-1)
given('1.0.0-alpha', '1.0.0-alpha2').expect(-1)
given('1.0.0-alpha2', '1.0.0-beta').expect(-1)
given('1.0.0-beta', '1.0.0-beta2').expect(-1)
given('1.0.0-beta2', '1.0.0-RC').expect(-1)
given('1.0.0-RC', '1.0.0-RC2').expect(-1)
given('1.0.0-RC2', '1.0.0').expect(-1)
given('1.0.0', '1.0.0-patch').expect(-1)
given('1.0.0-patch', '1.0.0-dev').expect(-1)
given('1.0.0-dev', '1.0.1').expect(-1)
given('1.0.1', '1.0.x-dev').expect(-1)
// short versions should compare equal to long versions
given('1.0.0-p', '1.0.0-patch').expect(0)
given('1.0.0-a', '1.0.0-alpha').expect(0)
given('1.0.0-a2', '1.0.0-alpha2').expect(0)
given('1.0.0-b', '1.0.0-beta').expect(0)
given('1.0.0-b2', '1.0.0-beta2').expect(0)
// numeric suffixes
given('1.0.0-b1', '1.0.0-b2').expect(-1)
given('1.0.0-b10', '1.0.0-b11').expect(-1)
given('1.0.0-a1', '1.0.0-a2').expect(-1)
given('1.0.0-a10', '1.0.0-a11').expect(-1)
given('1.0.0-RC1', '1.0.0-RC2').expect(-1)
given('1.0.0-RC10', '1.0.0-RC11').expect(-1)
})
})

View File

@@ -1,206 +0,0 @@
/**
* Commonly-used functions for formatting text in badge labels. Includes
* ordinal numbers, currency codes, star ratings, versions, etc.
*/
'use strict'
const moment = require('moment')
moment().format()
function starRating(rating, max = 5) {
const flooredRating = Math.floor(rating)
let stars = ''
while (stars.length < flooredRating) {
stars += '★'
}
const decimal = rating - flooredRating
if (decimal >= 0.875) {
stars += '★'
} else if (decimal >= 0.625) {
stars += '¾'
} else if (decimal >= 0.375) {
stars += '½'
} else if (decimal >= 0.125) {
stars += '¼'
}
while (stars.length < max) {
stars += '☆'
}
return stars
}
// Convert ISO 4217 code to unicode string.
function currencyFromCode(code) {
return (
{
CNY: '¥',
EUR: '€',
GBP: '₤',
USD: '$',
}[code] || code
)
}
function ordinalNumber(n) {
const s = ['ᵗʰ', 'ˢᵗ', 'ⁿᵈ', 'ʳᵈ'],
v = n % 100
return n + (s[(v - 20) % 10] || s[v] || s[0])
}
// Given a number, string with appropriate unit in the metric system, SI.
// Note: numbers beyond the peta- cannot be represented as integers in JS.
const metricPrefix = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
const metricPower = metricPrefix.map((a, i) => Math.pow(1000, i + 1))
function metric(n) {
for (let i = metricPrefix.length - 1; i >= 0; i--) {
const limit = metricPower[i]
if (n >= limit) {
n = Math.round(n / limit)
if (n < 1000) {
return `${n}${metricPrefix[i]}`
} else {
return `1${metricPrefix[i + 1]}`
}
}
}
return `${n}`
}
// Remove the starting v in a string.
function omitv(version) {
if (version.charCodeAt(0) === 118) {
return version.slice(1)
}
return version
}
// Add a starting v to the version unless:
// - it does not start with a digit
// - it is a date (yyyy-mm-dd)
const ignoredVersionPatterns = /^[^0-9]|[0-9]{4}-[0-9]{2}-[0-9]{2}/
function addv(version) {
version = `${version}`
if (version.startsWith('v') || ignoredVersionPatterns.test(version)) {
return version
} else {
return `v${version}`
}
}
function maybePluralize(singular, countable, plural) {
plural = plural || `${singular}s`
if (countable && countable.length === 1) {
return singular
} else {
return plural
}
}
function formatDate(d) {
const date = moment(d)
const dateString = date.calendar(null, {
lastDay: '[yesterday]',
sameDay: '[today]',
lastWeek: '[last] dddd',
sameElse: 'MMMM YYYY',
})
// Trim current year from date string
return dateString.replace(` ${moment().year()}`, '').toLowerCase()
}
function formatRelativeDate(timestamp) {
return moment()
.to(moment.unix(parseInt(timestamp, 10)))
.toLowerCase()
}
function renderTestResultMessage({
passed,
failed,
skipped,
total,
passedLabel,
failedLabel,
skippedLabel,
isCompact,
}) {
const labels = { passedLabel, failedLabel, skippedLabel }
if (total === 0) {
return 'no tests'
} else if (isCompact) {
;({ passedLabel = '✔', failedLabel = '✘', skippedLabel = '➟' } = labels)
return [
`${passedLabel} ${passed}`,
failed > 0 && `${failedLabel} ${failed}`,
skipped > 0 && `${skippedLabel} ${skipped}`,
]
.filter(Boolean)
.join(' | ')
} else {
;({
passedLabel = 'passed',
failedLabel = 'failed',
skippedLabel = 'skipped',
} = labels)
return [
`${passed} ${passedLabel}`,
failed > 0 && `${failed} ${failedLabel}`,
skipped > 0 && `${skipped} ${skippedLabel}`,
]
.filter(Boolean)
.join(', ')
}
}
function renderTestResultBadge({
passed,
failed,
skipped,
total,
passedLabel,
failedLabel,
skippedLabel,
isCompact,
}) {
const message = renderTestResultMessage({
passed,
failed,
skipped,
total,
passedLabel,
failedLabel,
skippedLabel,
isCompact,
})
let color
if (total === 0) {
color = 'yellow'
} else if (failed > 0) {
color = 'red'
} else if (skipped > 0 && passed > 0) {
color = 'green'
} else if (skipped > 0) {
color = 'yellow'
} else {
color = 'brightgreen'
}
return { message, color }
}
module.exports = {
starRating,
currencyFromCode,
ordinalNumber,
metric,
omitv,
addv,
maybePluralize,
formatDate,
formatRelativeDate,
renderTestResultMessage,
renderTestResultBadge,
}

View File

@@ -1,168 +0,0 @@
'use strict'
const { test, given } = require('sazerac')
const sinon = require('sinon')
const {
starRating,
currencyFromCode,
ordinalNumber,
metric,
omitv,
addv,
maybePluralize,
formatDate,
formatRelativeDate,
renderTestResultMessage,
renderTestResultBadge,
} = require('./text-formatters')
describe('Text formatters', function() {
test(starRating, () => {
given(4.9).expect('★★★★★')
given(3.7).expect('★★★¾☆')
given(2.566).expect('★★½☆☆')
given(2.2).expect('★★¼☆☆')
given(3).expect('★★★☆☆')
given(2, 4).expect('★★☆☆')
})
test(currencyFromCode, () => {
given('CNY').expect('¥')
given('EUR').expect('€')
given('GBP').expect('₤')
given('USD').expect('$')
given('AUD').expect('AUD')
})
test(ordinalNumber, () => {
given(2).expect('2ⁿᵈ')
given(11).expect('11ᵗʰ')
given(23).expect('23ʳᵈ')
given(131).expect('131ˢᵗ')
})
test(metric, () => {
given(999).expect('999')
given(1000).expect('1k')
given(999499).expect('999k')
given(999500).expect('1M')
given(1578896212).expect('2G')
given(80000000000000).expect('80T')
given(4000000000000001).expect('4P')
given(71007000100580002000).expect('71E')
given(1000000000000000000000).expect('1Z')
given(2222222222222222222222222).expect('2Y')
})
test(omitv, () => {
given('hello').expect('hello')
given('v1.0.1').expect('1.0.1')
})
test(addv, () => {
given(9).expect('v9')
given(0.1).expect('v0.1')
given('1.0.0').expect('v1.0.0')
given('v0.6').expect('v0.6')
given('hello').expect('hello')
given('2017-05-05-Release-2.3.17').expect('2017-05-05-Release-2.3.17')
})
test(maybePluralize, () => {
given('foo', []).expect('foos')
given('foo', [123]).expect('foo')
given('foo', [123, 456]).expect('foos')
given('foo', undefined).expect('foos')
given('box', [], 'boxes').expect('boxes')
given('box', [123], 'boxes').expect('box')
given('box', [123, 456], 'boxes').expect('boxes')
given('box', undefined, 'boxes').expect('boxes')
})
test(formatDate, () => {
given(1465513200000)
.describe('when given a timestamp in june 2016')
.expect('june 2016')
})
context('in october', function() {
let clock
beforeEach(function() {
clock = sinon.useFakeTimers(new Date(2017, 9, 15).getTime())
})
afterEach(function() {
clock.restore()
})
test(formatDate, () => {
given(new Date(2017, 0, 1).getTime())
.describe('when given the beginning of this year')
.expect('january')
})
})
context('in october', function() {
let clock
beforeEach(function() {
clock = sinon.useFakeTimers(new Date(2018, 9, 29).getTime())
})
afterEach(function() {
clock.restore()
})
test(formatRelativeDate, () => {
given(new Date(2018, 9, 31).getTime() / 1000)
.describe('when given the end of october')
.expect('in 2 days')
})
test(formatRelativeDate, () => {
given(new Date(2018, 9, 1).getTime() / 1000)
.describe('when given the beginning of october')
.expect('a month ago')
})
})
function renderBothStyles(props) {
const { message: standardMessage, color } = renderTestResultBadge(props)
const compactMessage = renderTestResultMessage({
...props,
isCompact: true,
})
return { standardMessage, compactMessage, color }
}
test(renderBothStyles, () => {
given({ passed: 12, failed: 3, skipped: 3, total: 18 }).expect({
standardMessage: '12 passed, 3 failed, 3 skipped',
compactMessage: '✔ 12 | ✘ 3 | ➟ 3',
color: 'red',
})
given({ passed: 12, failed: 3, skipped: 0, total: 15 }).expect({
standardMessage: '12 passed, 3 failed',
compactMessage: '✔ 12 | ✘ 3',
color: 'red',
})
given({ passed: 12, failed: 0, skipped: 3, total: 15 }).expect({
standardMessage: '12 passed, 3 skipped',
compactMessage: '✔ 12 | ➟ 3',
color: 'green',
})
given({ passed: 0, failed: 0, skipped: 3, total: 3 }).expect({
standardMessage: '0 passed, 3 skipped',
compactMessage: '✔ 0 | ➟ 3',
color: 'yellow',
})
given({ passed: 12, failed: 0, skipped: 0, total: 12 }).expect({
standardMessage: '12 passed',
compactMessage: '✔ 12',
color: 'brightgreen',
})
given({ passed: 0, failed: 0, skipped: 0, total: 0 }).expect({
standardMessage: 'no tests',
compactMessage: 'no tests',
color: 'yellow',
})
})
})

View File

@@ -1,7 +0,0 @@
'use strict'
// Cause unhandled promise rejections to fail unit tests, and print with stack
// traces.
process.on('unhandledRejection', error => {
throw error
})

View File

@@ -1,154 +0,0 @@
/**
* Utilities relating to generating badges relating to version numbers. Includes
* comparing versions to determine the latest, and determining the color to use
* for the badge based on whether the version is a stable release.
*
* For utilities specific to PHP version ranges, see php-version.js.
*/
'use strict'
const semver = require('semver')
const { addv } = require('./text-formatters')
const { version: versionColor } = require('./color-formatters')
// Given a list of versions (as strings), return the latest version.
// Return undefined if no version could be found.
function latest(versions, { pre = false } = {}) {
let version = ''
let origVersions = versions
// return all results that are likely semver compatible versions
versions = origVersions.filter(version => /\d+\.\d+/.test(version))
// If no semver versions then look for single numbered versions
if (!versions.length) {
versions = origVersions.filter(version => /\d+/.test(version))
}
if (!pre) {
// remove pre-releases from array
versions = versions.filter(version => !/\d+-\w+/.test(version))
}
try {
// coerce to string then lowercase otherwise alpha > RC
version = versions.sort((a, b) =>
semver.rcompare(
`${a}`.toLowerCase(),
`${b}`.toLowerCase(),
/* loose */ true
)
)[0]
} catch (e) {
version = latestDottedVersion(versions)
}
if (version === undefined || version === null) {
origVersions = origVersions.sort()
version = origVersions[origVersions.length - 1]
}
return version
}
function listCompare(a, b) {
const alen = a.length,
blen = b.length
for (let i = 0; i < alen; i++) {
if (a[i] < b[i]) {
return -1
} else if (a[i] > b[i]) {
return 1
}
}
return alen - blen
}
// === Private helper functions ===
// Take a list of string versions.
// Return the latest, or undefined, if there are none.
function latestDottedVersion(versions) {
const len = versions.length
if (len === 0) {
return
}
let version = versions[0]
for (let i = 1; i < len; i++) {
if (compareDottedVersion(version, versions[i]) < 0) {
version = versions[i]
}
}
return version
}
// Take string versions.
// -1 if v1 < v2, 1 if v1 > v2, 0 otherwise.
function compareDottedVersion(v1, v2) {
const parts1 = /([0-9.]+)(.*)$/.exec(v1)
const parts2 = /([0-9.]+)(.*)$/.exec(v2)
if (parts1 != null && parts2 != null) {
const numbers1 = parts1[1]
const numbers2 = parts2[1]
const distinguisher1 = parts1[2]
const distinguisher2 = parts2[2]
const numlist1 = numbers1.split('.').map(e => +e)
const numlist2 = numbers2.split('.').map(e => +e)
const cmp = listCompare(numlist1, numlist2)
if (cmp !== 0) {
return cmp
} else {
return distinguisher1 < distinguisher2
? -1
: distinguisher1 > distinguisher2
? 1
: 0
}
}
return v1 < v2 ? -1 : v1 > v2 ? 1 : 0
}
// Slice the specified number of dotted parts from the given semver version.
// e.g. slice('2.4.7', 'minor') -> '2.4'
function slice(v, releaseType) {
if (!semver.valid(v, /* loose */ true)) {
return null
}
const major = semver.major(v, /* loose */ true)
const minor = semver.minor(v, /* loose */ true)
const patch = semver.patch(v, /* loose */ true)
const prerelease = semver.prerelease(v, /* loose */ true)
const dottedParts = {
major: [major],
minor: [major, minor],
patch: [major, minor, patch],
}[releaseType]
if (dottedParts === undefined) {
throw Error(`Unknown releaseType: ${releaseType}`)
}
const dotted = dottedParts.join('.')
if (prerelease) {
return `${dotted}-${prerelease.join('.')}`
} else {
return dotted
}
}
function rangeStart(v) {
const range = new semver.Range(v, /* loose */ true)
return range.set[0][0].semver.version
}
function renderVersionBadge({ version, tag, defaultLabel }) {
return {
label: tag ? `${defaultLabel}@${tag}` : undefined,
message: addv(version),
color: versionColor(version),
}
}
module.exports = {
latest,
listCompare,
slice,
rangeStart,
renderVersionBadge,
}

View File

@@ -1,135 +0,0 @@
'use strict'
const { test, given } = require('sazerac')
const { latest, slice, rangeStart, renderVersionBadge } = require('./version')
const includePre = true
describe('Version helpers', function() {
test(latest, () => {
// semver-compatible versions.
given(['1.0.0', '1.0.2', '1.0.1']).expect('1.0.2')
given(['1.0.0', '2.0.0', '3.0.0']).expect('3.0.0')
given(['0.0.1', '0.0.10', '0.0.2', '0.0.20']).expect('0.0.20')
// "not-quite-valid" semver versions
given(['1.0.00', '1.0.02', '1.0.01']).expect('1.0.02')
given(['1.0.05', '2.0.05', '3.0.05']).expect('3.0.05')
given(['0.0.01', '0.0.010', '0.0.02', '0.0.020']).expect('0.0.020')
// Mixed style versions. - include pre-releases
given(['1.0.0', 'v1.0.2', 'r1.0.1', 'release-2.0.0', 'v1.0.1-alpha.1'], {
pre: includePre,
}).expect('release-2.0.0')
given(['1.0.0', 'v2.0.0', 'r1.0.1', 'release-1.0.3', 'v1.0.1-alpha.1'], {
pre: includePre,
}).expect('v2.0.0')
given(['2.0.0', 'v1.0.3', 'r1.0.1', 'release-1.0.3', 'v1.0.1-alpha.1'], {
pre: includePre,
}).expect('2.0.0')
given(['1.0.0', 'v1.0.2', 'r2.0.0', 'release-1.0.3', 'v1.0.1-alpha.1'], {
pre: includePre,
}).expect('r2.0.0')
given(['1.0.0', 'v1.0.2', 'r2.0.0', 'release-1.0.3', 'v2.0.1-alpha.1'], {
pre: includePre,
}).expect('v2.0.1-alpha.1')
// Versions with 'v' prefix.
given(['v1.0.0', 'v1.0.2', 'v1.0.1']).expect('v1.0.2')
given(['v1.0.0', 'v3.0.0', 'v2.0.0']).expect('v3.0.0')
// Simple (2 number) versions.
given(['0.1', '0.3', '0.2']).expect('0.3')
given(['0.1', '0.5', '0.12', '0.21']).expect('0.21')
given(['1.0', '2.0', '3.0']).expect('3.0')
// Simple (one-number) versions
given(['2', '10', '1']).expect('10')
// Include pre-releases
given(
[
'v1.0.1-alpha.2',
'v1.0.1-alpha.1',
'v1.0.1-beta.3',
'v1.0.1-beta.1',
'v1.0.1-RC.1',
'v1.0.1-RC.2',
'v1.0.0',
],
{ pre: includePre }
).expect('v1.0.1-RC.2')
given(
[
'v1.0.1-alpha.2',
'v1.0.1-alpha.1',
'v1.0.1-beta.3',
'v1.0.1-beta.1',
'v1.0.1-RC.1',
'v1.0.1-RC.2',
'v1.0.1',
],
{ pre: includePre }
).expect('v1.0.1')
// Exclude pre-releases
given([
'v1.0.1-alpha.2',
'v1.0.1-alpha.1',
'v1.0.1-beta.3',
'v1.0.1-beta.1',
'v1.0.1-RC.1',
'v1.0.1-RC.2',
'v1.0.0',
]).expect('v1.0.0')
given([
'v1.0.1-alpha.2',
'v1.0.1-alpha.1',
'v1.0.1-beta.3',
'v1.0.1-beta.1',
'v1.0.1-RC.1',
'v1.0.1-RC.2',
'v1.0.1',
]).expect('v1.0.1')
// Versions with 'release-' prefix
given([
'release-1.0.0',
'release-1.0.2',
'release-1.0.20',
'release-1.0.3',
]).expect('release-1.0.20')
// Semver mixed with non semver versions
given(['1.0.0', '1.0.2', '1.1', '1.0', 'notaversion2', '12bcde4']).expect(
'1.1'
)
})
test(slice, () => {
given('2.4.7', 'major').expect('2')
given('2.4.7', 'minor').expect('2.4')
given('2.4.7', 'patch').expect('2.4.7')
given('02.4.7', 'major').expect('2')
given('2.04.7', 'minor').expect('2.4')
given('2.4.07', 'patch').expect('2.4.7')
given('2.4.7-alpha.1', 'major').expect('2-alpha.1')
given('2.4.7-alpha.1', 'minor').expect('2.4-alpha.1')
given('2.4.7-alpha.1', 'patch').expect('2.4.7-alpha.1')
})
test(rangeStart, () => {
given('^2.4.7').expect('2.4.7')
})
test(renderVersionBadge, () => {
given({ version: '1.2.3' }).expect({
label: undefined,
message: 'v1.2.3',
color: 'blue',
})
given({ version: '1.2.3', tag: 'next', defaultLabel: 'npm' }).expect({
label: 'npm@next',
message: 'v1.2.3',
color: 'blue',
})
})
})