Skip to content

Commit

Permalink
Merge pull request #9731 from bertdeblock/debug-utils
Browse files Browse the repository at this point in the history
Add an `assert` and a `deprecate` utility
  • Loading branch information
rwjblue committed Jan 5, 2022
2 parents e9651d8 + 01577b4 commit 1b7f496
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 0 deletions.
37 changes: 37 additions & 0 deletions lib/debug/assert.js
@@ -0,0 +1,37 @@
'use strict';

/**
* Verify that a certain condition is met, or throw an error if otherwise.
*
* This is useful for communicating expectations in the code to other human
* readers as well as catching bugs that accidentally violate these expectations.
*
* ```js
* const { assert } = require('ember-cli/lib/debug');
*
* // Test for truthiness:
* assert('Must pass a string.', typeof str === 'string');
*
* // Fail unconditionally:
* assert('This code path should never run.');
* ```
*
* @method assert
* @param {String} description Describes the condition.
* This will become the message of the error thrown if the assertion fails.
* @param {Any} condition Must be truthy for the assertion to pass.
* If falsy, an error will be thrown.
*/
function assert(description, condition) {
if (!description) {
throw new Error('When calling `assert`, you must provide a description as the first argument.');
}

if (condition) {
return;
}

throw new Error(`ASSERTION FAILED: ${description}`);
}

module.exports = assert;
111 changes: 111 additions & 0 deletions lib/debug/deprecate.js
@@ -0,0 +1,111 @@
'use strict';

const chalk = require('chalk');
const semver = require('semver');
const assert = require('./assert');

/**
* Display a deprecation message.
*
* ```js
* const { deprecate } = require('ember-cli/lib/debug');
*
* deprecate('The `foo` method is deprecated.', false, {
* for: 'ember-cli',
* id: 'ember-cli.foo-method',
* since: {
* available: '4.1.0',
* enabled: '4.2.0',
* },
* until: '5.0.0',
* url: 'https://example.com',
* });
* ```
*
* @method deprecate
* @param {String} description Describes the deprecation.
* @param {Any} condition If falsy, the deprecation message will be displayed.
* @param {Object} options An object including the deprecation's details:
* - `for` The library that the deprecation is for
* - `id` The deprecation's unique id
* - `since.available` A SemVer version indicating when the deprecation was made available
* - `since.enabled` A SemVer version indicating when the deprecation was enabled
* - `until` A SemVer version indicating until when the deprecation will be active
* - `url` A URL that refers to additional information about the deprecation
*/
function deprecate(description, condition, options) {
assert('When calling `deprecate`, you must provide a description as the first argument.', description);
assert('When calling `deprecate`, you must provide a condition as the second argument.', arguments.length > 1);

assert(
'When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).',
options
);

assert('When calling `deprecate`, you must provide the `for` option.', options.for);
assert('When calling `deprecate`, you must provide the `id` option.', options.id);

assert(
'When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.',
options.since
);

assert(
'When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.',
options.since.available || options.since.enabled
);

assert(
'`since.available` must be a valid SemVer version.',
!options.since.available || isSemVer(options.since.available)
);

assert('`since.enabled` must be a valid SemVer version.', !options.since.enabled || isSemVer(options.since.enabled));

assert(
'When calling `deprecate`, you must provide a valid SemVer version for the `until` option.',
isSemVer(options.until)
);

if (condition) {
return;
}

let message = formatMessage(description, options);

warn(message);
warn(getStackTrace());

// Return the message for testing purposes.
// This can be removed once we can register deprecation handlers.
return message;
}

function isSemVer(version) {
return semver.valid(version) !== null;
}

function formatMessage(description, options) {
let message = [`DEPRECATION: ${description}`, `[ID: ${options.id}]`];

if (options.url) {
message.push(`See ${options.url} for more details.`);
}

return message.join(' ');
}

function getStackTrace() {
let error = new Error();
let lines = error.stack.split('\n');

lines.shift(); // Remove the word `Error`.

return lines.map((line) => line.trim()).join('\n');
}

function warn(message) {
console.warn(chalk.yellow(message));
}

module.exports = deprecate;
6 changes: 6 additions & 0 deletions lib/debug/index.js
@@ -0,0 +1,6 @@
'use strict';

module.exports = {
assert: require('./assert'),
deprecate: require('./deprecate'),
};
44 changes: 44 additions & 0 deletions tests/unit/debug/assert-test.js
@@ -0,0 +1,44 @@
'use strict';

const { expect } = require('chai');
const { assert } = require('../../../lib/debug');

describe('assert', function () {
it('it throws when the description argument is missing', function () {
expect(() => {
assert();
}).to.throw('When calling `assert`, you must provide a description as the first argument.');

expect(() => {
assert('');
}).to.throw('When calling `assert`, you must provide a description as the first argument.');
});

it('it does nothing when the condition argument is truthy', function () {
expect(() => {
assert('description', 1);
}).to.not.throw();

expect(() => {
assert('description', {});
}).to.not.throw();

expect(() => {
assert('description', true);
}).to.not.throw();
});

it('it throws when the condition argument is falsy', function () {
expect(() => {
assert('description');
}).to.throw('ASSERTION FAILED: description');

expect(() => {
assert('description', null);
}).to.throw('ASSERTION FAILED: description');

expect(() => {
assert('description', false);
}).to.throw('ASSERTION FAILED: description');
});
});
175 changes: 175 additions & 0 deletions tests/unit/debug/deprecate-test.js
@@ -0,0 +1,175 @@
'use strict';

const { expect } = require('chai');
const { deprecate } = require('../../../lib/debug');

describe('deprecate', function () {
it('it throws when the description argument is missing', function () {
expect(() => {
deprecate();
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a description as the first argument.');

expect(() => {
deprecate('');
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a description as the first argument.');
});

it('it throws when the condition argument is missing', function () {
expect(() => {
deprecate('description');
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide a condition as the second argument.');
});

it('it throws when the options argument is missing', function () {
expect(() => {
deprecate('description', true);
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).'
);

expect(() => {
deprecate('description', undefined);
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).'
);

expect(() => {
deprecate('description', false, null);
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide an options object as the third argument. The options object must include the `for`, `id`, `since` and `until` options (`url` is optional).'
);
});

it('it throws when the `for` option is missing', function () {
expect(() => {
deprecate('description', true, {});
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide the `for` option.');
});

it('it throws when the `id` option is missing', function () {
expect(() => {
deprecate('description', true, {
for: 'foo',
});
}).to.throw('ASSERTION FAILED: When calling `deprecate`, you must provide the `id` option.');
});

it('it throws when the `since` option is missing', function () {
expect(() => {
deprecate('description', true, {
for: 'foo',
id: 'foo',
});
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide the `since` option. `since` must include the `available` and/or the `enabled` option.'
);
});

it('it throws when both the `since.available` and `since.enabled` options are missing', function () {
expect(() => {
deprecate('description', true, {
for: 'foo',
id: 'foo',
since: {},
});
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide the `since.available` and/or the `since.enabled` option.'
);
});

it('it throws when the `since.available` option is not a valid SemVer version', function () {
expect(() => {
deprecate('description', true, {
for: 'foo',
id: 'foo',
since: {
available: 'foo',
},
});
}).to.throw('ASSERTION FAILED: `since.available` must be a valid SemVer version.');
});

it('it throws when the `since.enabled` option is not a valid SemVer version', function () {
expect(() => {
deprecate('description', true, {
for: 'foo',
id: 'foo',
since: {
enabled: 'foo',
},
});
}).to.throw('ASSERTION FAILED: `since.enabled` must be a valid SemVer version.');
});

it('it throws when the `until` option is not a valid SemVer version', function () {
expect(() => {
deprecate('description', true, {
for: 'foo',
id: 'foo',
since: {
available: '4.0.0',
enabled: '4.0.0',
},
});
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide a valid SemVer version for the `until` option.'
);

expect(() => {
deprecate('description', true, {
for: 'foo',
id: 'foo',
since: {
available: '4.0.0',
enabled: '4.0.0',
},
until: 'foo',
});
}).to.throw(
'ASSERTION FAILED: When calling `deprecate`, you must provide a valid SemVer version for the `until` option.'
);
});

it('it does nothing when the condition argument is truthy', function () {
let message = deprecate('description', true, {
for: 'foo',
id: 'foo',
since: {
available: '4.0.0',
enabled: '4.0.0',
},
until: '5.0.0',
});

expect(message).to.be.undefined;
});

it('it displays a deprecation message when the condition argument is falsy', function () {
let message = deprecate('description', false, {
for: 'foo',
id: 'foo',
since: {
available: '4.0.0',
enabled: '4.0.0',
},
until: '5.0.0',
});

expect(message).to.be.equal('DEPRECATION: description [ID: foo]');
});

it('it includes the `url` option in the deprecation message when provided', function () {
let message = deprecate('description', false, {
for: 'foo',
id: 'foo',
since: {
available: '4.0.0',
enabled: '4.0.0',
},
until: '5.0.0',
url: 'https://example.com',
});

expect(message).to.be.equal('DEPRECATION: description [ID: foo] See https://example.com for more details.');
});
});

0 comments on commit 1b7f496

Please sign in to comment.