From 3c20ab7634e49c9eb3919f4da43681a8703020ef Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Wed, 24 Feb 2021 10:51:30 -0500 Subject: [PATCH] chore: use semver to determine API compatibility --- src/api/context.ts | 30 ++----- src/api/diag.ts | 56 +++++-------- src/api/global-utils.ts | 70 ---------------- src/api/propagation.ts | 31 ++----- src/api/trace.ts | 34 +++----- src/internal/global-utils.ts | 92 +++++++++++++++++++++ src/internal/semver.ts | 113 ++++++++++++++++++++++++++ test/api/api.test.ts | 1 - test/diag/logLevel.test.ts | 2 +- test/{api => internal}/global.test.ts | 66 +++++++++++++-- test/internal/semver.test.ts | 101 +++++++++++++++++++++++ 11 files changed, 413 insertions(+), 183 deletions(-) delete mode 100644 src/api/global-utils.ts create mode 100644 src/internal/global-utils.ts create mode 100644 src/internal/semver.ts rename test/{api => internal}/global.test.ts (58%) create mode 100644 test/internal/semver.test.ts diff --git a/src/api/context.ts b/src/api/context.ts index f5c10a93..d6ea2445 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -17,12 +17,12 @@ import { NoopContextManager } from '../context/NoopContextManager'; import { Context, ContextManager } from '../context/types'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_CONTEXT_MANAGER_API_KEY, - makeGetter, - _global, -} from './global-utils'; + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; +const API_NAME = 'context'; const NOOP_CONTEXT_MANAGER = new NoopContextManager(); /** @@ -49,17 +49,7 @@ export class ContextAPI { public setGlobalContextManager( contextManager: ContextManager ): ContextManager { - if (_global[GLOBAL_CONTEXT_MANAGER_API_KEY]) { - // global context manager has already been set - return this._getContextManager(); - } - - _global[GLOBAL_CONTEXT_MANAGER_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - contextManager, - NOOP_CONTEXT_MANAGER - ); - + registerGlobal(API_NAME, contextManager); return contextManager; } @@ -98,16 +88,12 @@ export class ContextAPI { } private _getContextManager(): ContextManager { - return ( - _global[GLOBAL_CONTEXT_MANAGER_API_KEY]?.( - API_BACKWARDS_COMPATIBILITY_VERSION - ) ?? NOOP_CONTEXT_MANAGER - ); + return getGlobal(API_NAME) || NOOP_CONTEXT_MANAGER; } /** Disable and remove the global context manager */ public disable() { this._getContextManager().disable(); - delete _global[GLOBAL_CONTEXT_MANAGER_API_KEY]; + unregisterGlobal(API_NAME); } } diff --git a/src/api/diag.ts b/src/api/diag.ts index 760826d6..f06a4c5a 100644 --- a/src/api/diag.ts +++ b/src/api/diag.ts @@ -23,48 +23,27 @@ import { } from '../diag/logger'; import { DiagLogLevel, createLogLevelDiagLogger } from '../diag/logLevel'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_DIAG_LOGGER_API_KEY, - makeGetter, - _global, -} from './global-utils'; - -/** Internal simple Noop Diag API that returns a noop logger and does not allow any changes */ -function noopDiagApi(): DiagAPI { - const noopLogger = createNoopDiagLogger(); - return { - disable: () => {}, - getLogger: () => noopLogger, - setLogger: () => {}, - ...noopLogger, - }; -} + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; + +const API_NAME = 'diag'; /** * Singleton object which represents the entry point to the OpenTelemetry internal * diagnostic API */ export class DiagAPI implements DiagLogger { + private static _instance?: DiagAPI; + /** Get the singleton instance of the DiagAPI API */ public static instance(): DiagAPI { - let theInst = null; - if (_global[GLOBAL_DIAG_LOGGER_API_KEY]) { - // Looks like a previous instance was set, so try and fetch it - theInst = _global[GLOBAL_DIAG_LOGGER_API_KEY]?.( - API_BACKWARDS_COMPATIBILITY_VERSION - ) as DiagAPI; + if (!this._instance) { + this._instance = new DiagAPI(); } - if (!theInst) { - theInst = new DiagAPI(); - _global[GLOBAL_DIAG_LOGGER_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - theInst, - noopDiagApi() - ); - } - - return theInst; + return this._instance; } /** @@ -73,12 +52,11 @@ export class DiagAPI implements DiagLogger { */ private constructor() { const _noopLogger = createNoopDiagLogger(); - let _filteredLogger: FilteredDiagLogger | undefined; function _logProxy(funcName: keyof DiagLogger): DiagLogFunction { return function () { const orgArguments = arguments as unknown; - const theLogger = _filteredLogger || _noopLogger; + const theLogger = self.getLogger(); const theFunc = theLogger[funcName]; if (typeof theFunc === 'function') { return theFunc.apply( @@ -95,7 +73,7 @@ export class DiagAPI implements DiagLogger { // DiagAPI specific functions self.getLogger = (): FilteredDiagLogger => { - return _filteredLogger || _noopLogger; + return getGlobal(API_NAME) || _noopLogger; }; self.setLogger = ( @@ -106,11 +84,15 @@ export class DiagAPI implements DiagLogger { // is used as a child of itself accidentally. logger = logger === self ? self.getLogger().getChild() : logger; logger = logger ?? _noopLogger; - _filteredLogger = createLogLevelDiagLogger(logLevel, logger); + registerGlobal( + API_NAME, + createLogLevelDiagLogger(logLevel, logger), + true + ); }; self.disable = () => { - _filteredLogger = undefined; + unregisterGlobal(API_NAME); }; for (let i = 0; i < diagLoggerFunctions.length; i++) { diff --git a/src/api/global-utils.ts b/src/api/global-utils.ts deleted file mode 100644 index 5c86e6ed..00000000 --- a/src/api/global-utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DiagAPI } from '../api/diag'; -import { ContextManager } from '../context/types'; -import { _globalThis } from '../platform'; -import { TextMapPropagator } from '../propagation/TextMapPropagator'; -import { TracerProvider } from '../trace/tracer_provider'; - -export const GLOBAL_CONTEXT_MANAGER_API_KEY = Symbol.for( - 'io.opentelemetry.js.api.context' -); - -export const GLOBAL_PROPAGATION_API_KEY = Symbol.for( - 'io.opentelemetry.js.api.propagation' -); -export const GLOBAL_TRACE_API_KEY = Symbol.for('io.opentelemetry.js.api.trace'); - -export const GLOBAL_DIAG_LOGGER_API_KEY = Symbol.for( - 'io.opentelemetry.js.api.diag' -); - -type Get = (version: number) => T; -type OtelGlobal = Partial<{ - [GLOBAL_CONTEXT_MANAGER_API_KEY]: Get; - [GLOBAL_PROPAGATION_API_KEY]: Get; - [GLOBAL_TRACE_API_KEY]: Get; - [GLOBAL_DIAG_LOGGER_API_KEY]: Get; -}>; - -export const _global = _globalThis as OtelGlobal; - -/** - * Make a function which accepts a version integer and returns the instance of an API if the version - * is compatible, or a fallback version (usually NOOP) if it is not. - * - * @param requiredVersion Backwards compatibility version which is required to return the instance - * @param instance Instance which should be returned if the required version is compatible - * @param fallback Fallback instance, usually NOOP, which will be returned if the required version is not compatible - */ -export function makeGetter( - requiredVersion: number, - instance: T, - fallback: T -): Get { - return (version: number): T => - version === requiredVersion ? instance : fallback; -} - -/** - * A number which should be incremented each time a backwards incompatible - * change is made to the API. This number is used when an API package - * attempts to access the global API to ensure it is getting a compatible - * version. If the global API is not compatible with the API package - * attempting to get it, a NOOP API implementation will be returned. - */ -export const API_BACKWARDS_COMPATIBILITY_VERSION = 5; diff --git a/src/api/propagation.ts b/src/api/propagation.ts index dd214413..ee6394d2 100644 --- a/src/api/propagation.ts +++ b/src/api/propagation.ts @@ -24,11 +24,12 @@ import { TextMapSetter, } from '../propagation/TextMapPropagator'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_PROPAGATION_API_KEY, - makeGetter, - _global, -} from './global-utils'; + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; + +const API_NAME = 'propagation'; /** * Singleton object which represents the entry point to the OpenTelemetry Propagation API @@ -52,17 +53,7 @@ export class PropagationAPI { * Set the current propagator. Returns the initialized propagator */ public setGlobalPropagator(propagator: TextMapPropagator): TextMapPropagator { - if (_global[GLOBAL_PROPAGATION_API_KEY]) { - // global propagator has already been set - return this._getGlobalPropagator(); - } - - _global[GLOBAL_PROPAGATION_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - propagator, - NOOP_TEXT_MAP_PROPAGATOR - ); - + registerGlobal(API_NAME, propagator); return propagator; } @@ -105,14 +96,10 @@ export class PropagationAPI { /** Remove the global propagator */ public disable() { - delete _global[GLOBAL_PROPAGATION_API_KEY]; + unregisterGlobal(API_NAME); } private _getGlobalPropagator(): TextMapPropagator { - return ( - _global[GLOBAL_PROPAGATION_API_KEY]?.( - API_BACKWARDS_COMPATIBILITY_VERSION - ) ?? NOOP_TEXT_MAP_PROPAGATOR - ); + return getGlobal(API_NAME) || NOOP_TEXT_MAP_PROPAGATOR; } } diff --git a/src/api/trace.ts b/src/api/trace.ts index f0c20055..f9beba0c 100644 --- a/src/api/trace.ts +++ b/src/api/trace.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { NOOP_TRACER_PROVIDER } from '../trace/NoopTracerProvider'; import { ProxyTracerProvider } from '../trace/ProxyTracerProvider'; import { Tracer } from '../trace/tracer'; import { TracerProvider } from '../trace/tracer_provider'; import { isSpanContextValid } from '../trace/spancontext-utils'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_TRACE_API_KEY, - makeGetter, - _global, -} from './global-utils'; + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; + +const API_NAME = 'trace'; /** * Singleton object which represents the entry point to the OpenTelemetry Tracing API @@ -50,30 +50,16 @@ export class TraceAPI { * Set the current global tracer. Returns the initialized global tracer provider */ public setGlobalTracerProvider(provider: TracerProvider): TracerProvider { - if (_global[GLOBAL_TRACE_API_KEY]) { - // global tracer provider has already been set - return this.getTracerProvider(); - } - this._proxyTracerProvider.setDelegate(provider); - - _global[GLOBAL_TRACE_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - this._proxyTracerProvider, - NOOP_TRACER_PROVIDER - ); - - return this.getTracerProvider(); + registerGlobal(API_NAME, this._proxyTracerProvider); + return this._proxyTracerProvider; } /** * Returns the global tracer provider. */ public getTracerProvider(): TracerProvider { - return ( - _global[GLOBAL_TRACE_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? - this._proxyTracerProvider - ); + return getGlobal(API_NAME) || this._proxyTracerProvider; } /** @@ -85,7 +71,7 @@ export class TraceAPI { /** Remove the global tracer provider */ public disable() { - delete _global[GLOBAL_TRACE_API_KEY]; + unregisterGlobal(API_NAME); this._proxyTracerProvider = new ProxyTracerProvider(); } diff --git a/src/internal/global-utils.ts b/src/internal/global-utils.ts new file mode 100644 index 00000000..9d1b55e5 --- /dev/null +++ b/src/internal/global-utils.ts @@ -0,0 +1,92 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '..'; +import { ContextManager } from '../context/types'; +import { FilteredDiagLogger } from '../diag/logger'; +import { _globalThis } from '../platform'; +import { TextMapPropagator } from '../propagation/TextMapPropagator'; +import type { TracerProvider } from '../trace/tracer_provider'; +import { VERSION } from '../version'; +import { isCompatible } from './semver'; + +const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for('io.opentelemetry.js.api'); + +const _global = _globalThis as OTelGlobal; + +export function registerGlobal( + type: Type, + instance: OTelGlobalAPI[Type], + allowOverride = false +): void { + _global[GLOBAL_OPENTELEMETRY_API_KEY] = _global[ + GLOBAL_OPENTELEMETRY_API_KEY + ] ?? { + version: VERSION, + }; + + const api = _global[GLOBAL_OPENTELEMETRY_API_KEY]!; + if (!allowOverride && api[type]) { + // already registered an API of this type + const err = new Error( + `@opentelemetry/api: Attempted duplicate registration of API: ${type}` + ); + diag.error(err.stack || err.message); + return; + } + + if (api.version != VERSION) { + // All registered APIs must be of the same version exactly + const err = new Error( + '@opentelemetry/api: All API registration versions must match' + ); + diag.error(err.stack || err.message); + return; + } + + api[type] = instance; +} + +export function getGlobal( + type: Type +): OTelGlobalAPI[Type] | undefined { + const version = _global[GLOBAL_OPENTELEMETRY_API_KEY]?.version; + if (!version || !isCompatible(version)) { + return; + } + return _global[GLOBAL_OPENTELEMETRY_API_KEY]?.[type]; +} + +export function unregisterGlobal(type: keyof OTelGlobalAPI) { + const api = _global[GLOBAL_OPENTELEMETRY_API_KEY]; + + if (api) { + delete api[type]; + } +} + +type OTelGlobal = { + [GLOBAL_OPENTELEMETRY_API_KEY]?: OTelGlobalAPI; +}; + +type OTelGlobalAPI = { + version: string; + + diag?: FilteredDiagLogger; + trace?: TracerProvider; + context?: ContextManager; + propagation?: TextMapPropagator; +}; diff --git a/src/internal/semver.ts b/src/internal/semver.ts new file mode 100644 index 00000000..fba640d4 --- /dev/null +++ b/src/internal/semver.ts @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { VERSION } from '../version'; + +const re = /^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/; + +/** + * Create a function to test an API version to see if it is compatible with the provided ownVersion. + * + * The returned function has the following semantics: + * - Exact match is always compatible + * - Major versions must always match + * - The minor version of the API module requesting access to the global API must be greater or equal to the minor version of this API + * - Patch and build tag differences are not considered at this time + * + * @param ownVersion version which should be checked against + */ +export function _makeCompatibilityCheck( + ownVersion: string +): (version: string) => boolean { + const acceptedVersions = new Set([ownVersion]); + const rejectedVersions = new Set(); + + const myVersionMatch = ownVersion.match(re); + if (!myVersionMatch) { + throw new Error('Cannot parse own version'); + } + + const ownVersionParsed = { + major: +myVersionMatch[1], + minor: +myVersionMatch[2], + patch: +myVersionMatch[3], + }; + + return function isCompatible(version: string): boolean { + if (acceptedVersions.has(version)) { + return true; + } + + if (rejectedVersions.has(version)) { + return false; + } + + const m = version.match(re); + if (!m) { + // cannot parse other version + rejectedVersions.add(version); + return false; + } + + const otherVersionParsed = { + major: +m[1], + minor: +m[2], + patch: +m[3], + }; + + // major versions must match + if (ownVersionParsed.major != otherVersionParsed.major) { + rejectedVersions.add(version); + return false; + } + + // if major version is 0, minor is treated like major and patch is treated like minor + if (ownVersionParsed.major === 0) { + if (ownVersionParsed.minor != otherVersionParsed.minor) { + rejectedVersions.add(version); + return false; + } + + if (ownVersionParsed.patch < otherVersionParsed.patch) { + rejectedVersions.add(version); + return false; + } + + acceptedVersions.add(version); + return true; + } + + if (ownVersionParsed.minor < otherVersionParsed.minor) { + rejectedVersions.add(version); + return false; + } + + acceptedVersions.add(version); + return true; + }; +} + +/** + * Test an API version to see if it is compatible with this API. + * + * - Exact match is always compatible + * - Major versions must always match + * - The minor version of the API module requesting access to the global API must be greater or equal to the minor version of this API + * - Patch and build tag differences are not considered at this time + * + * @param version version of the API requesting an instance of the global API + */ +export const isCompatible = _makeCompatibilityCheck(VERSION); diff --git a/test/api/api.test.ts b/test/api/api.test.ts index 05e09425..e238b2f2 100644 --- a/test/api/api.test.ts +++ b/test/api/api.test.ts @@ -35,7 +35,6 @@ import api, { diagLoggerFunctions, } from '../../src'; import { DiagAPI } from '../../src/api/diag'; -import { _global } from '../../src/api/global-utils'; import { NoopSpan } from '../../src/trace/NoopSpan'; describe('API', () => { diff --git a/test/diag/logLevel.test.ts b/test/diag/logLevel.test.ts index 627f339f..096892bb 100644 --- a/test/diag/logLevel.test.ts +++ b/test/diag/logLevel.test.ts @@ -241,7 +241,7 @@ describe('LogLevelFilter DiagLogger', () => { const testLogger = createLogLevelDiagLogger( map.level, - diag.getLogger().getChild() + dummyLogger ); testLogger[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { diff --git a/test/api/global.test.ts b/test/internal/global.test.ts similarity index 58% rename from test/api/global.test.ts rename to test/internal/global.test.ts index 44edfeff..27cf30bb 100644 --- a/test/api/global.test.ts +++ b/test/internal/global.test.ts @@ -15,11 +15,10 @@ */ import * as assert from 'assert'; -import { - GLOBAL_CONTEXT_MANAGER_API_KEY, - _global, -} from '../../src/api/global-utils'; +import { getGlobal } from '../../src/internal/global-utils'; +import { _globalThis } from '../../src/platform'; import { NoopContextManager } from '../../src/context/NoopContextManager'; +import sinon = require('sinon'); const api1 = require('../../src') as typeof import('../../src'); @@ -42,6 +41,9 @@ describe('Global Utils', () => { api1.context.disable(); api1.propagation.disable(); api1.trace.disable(); + api1.diag.disable(); + // @ts-expect-error we are modifying internals for testing purposes here + delete _globalThis[Symbol.for('io.opentelemetry.js.api')]; }); it('should change the global context manager', () => { @@ -72,9 +74,61 @@ describe('Global Utils', () => { it('should return the module NoOp implementation if the version is a mismatch', () => { const original = api1.context['_getContextManager'](); + const newContextManager = new NoopContextManager(); + api1.context.setGlobalContextManager(newContextManager); + + assert.strictEqual(api1.context['_getContextManager'](), newContextManager); + + const globalInstance = getGlobal('context'); + assert.ok(globalInstance); + // @ts-expect-error we are modifying internals for testing purposes here + _globalThis[Symbol.for('io.opentelemetry.js.api')].version = '0.0.1'; + + assert.strictEqual(api1.context['_getContextManager'](), original); + }); + + it('should log an error if there is a duplicate registration', () => { + const error = sinon.stub(); + api1.diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error, + }); + + api1.context.setGlobalContextManager(new NoopContextManager()); api1.context.setGlobalContextManager(new NoopContextManager()); - const afterSet = _global[GLOBAL_CONTEXT_MANAGER_API_KEY]!(-1); - assert.strictEqual(original, afterSet); + sinon.assert.calledOnce(error); + assert.strictEqual(error.firstCall.args.length, 1); + assert.ok( + error.firstCall.args[0].startsWith( + 'Error: @opentelemetry/api: Attempted duplicate registration of API: context' + ) + ); + }); + + it('should allow duplicate registration of the diag logger', () => { + const error1 = sinon.stub(); + const error2 = sinon.stub(); + api1.diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: error1, + }); + + api1.diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: error2, + }); + + sinon.assert.notCalled(error1); + sinon.assert.notCalled(error2); }); }); diff --git a/test/internal/semver.test.ts b/test/internal/semver.test.ts new file mode 100644 index 00000000..6020723b --- /dev/null +++ b/test/internal/semver.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import { + isCompatible, + _makeCompatibilityCheck, +} from '../../src/internal/semver'; +import { VERSION } from '../../src/version'; + +describe('Version Compatibility', () => { + it('should be compatible if versions are equal', () => { + assert.ok(isCompatible(VERSION)); + }); + + describe('throws if own version cannot be parsed', () => { + assert.throws(() => { + _makeCompatibilityCheck('this is not semver'); + }); + }); + + describe('incompatible if other version cannot be parsed', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(!check('this is not semver')); + }); + + describe('>= 1.x', () => { + it('should be compatible if major and minor versions are equal', () => { + const check = _makeCompatibilityCheck('1.2.3'); + assert.ok(check('1.2.2')); + assert.ok(check('1.2.2-alpha')); + assert.ok(check('1.2.4')); + assert.ok(check('1.2.4-alpha')); + }); + + it('should be compatible if major versions are equal and minor version is lesser', () => { + const check = _makeCompatibilityCheck('1.2.3'); + assert.ok(check('1.1.2')); + assert.ok(check('1.1.2-alpha')); + assert.ok(check('1.1.4')); + assert.ok(check('1.1.4-alpha')); + }); + + it('should be incompatible if major versions do not match', () => { + const check = _makeCompatibilityCheck('3.3.3'); + assert.ok(!check('0.3.3')); + assert.ok(!check('0.3.3')); + }); + + it('should be incompatible if major versions match but other minor version is greater than our minor version', () => { + const check = _makeCompatibilityCheck('1.2.3'); + assert.ok(!check('1.3.3-alpha')); + assert.ok(!check('1.3.3')); + }); + }); + + describe('0.x', () => { + it('should be compatible if minor and patch versions are equal', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(check('0.1.2')); + assert.ok(check('0.1.2-alpha')); + }); + + it('should be compatible if minor versions are equal and patch version is lesser', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(check('0.1.1')); + assert.ok(check('0.1.1-alpha')); + }); + + it('should be incompatible if minor versions do not match', () => { + const check = _makeCompatibilityCheck('0.3.3'); + assert.ok(!check('0.2.3')); + assert.ok(!check('0.4.3')); + }); + + it('should be incompatible if minor versions do not match', () => { + const check = _makeCompatibilityCheck('0.3.3'); + assert.ok(!check('0.2.3')); + assert.ok(!check('0.4.3')); + }); + + it('should be incompatible if minor versions match but other patch version is greater than our patch version', () => { + const check = _makeCompatibilityCheck('0.3.3'); + assert.ok(!check('0.3.4-alpha')); + assert.ok(!check('0.3.4')); + }); + }); +});