diff --git a/api/src/internal/global-utils.ts b/api/src/internal/global-utils.ts index 68e7b9cae31..3a3579e12c2 100644 --- a/api/src/internal/global-utils.ts +++ b/api/src/internal/global-utils.ts @@ -66,8 +66,8 @@ export function registerGlobal( export function getGlobal( type: Type ): OTelGlobalAPI[Type] | undefined { - const version = _global[GLOBAL_OPENTELEMETRY_API_KEY]?.version; - if (!version || !isCompatible(version)) { + const globalVersion = _global[GLOBAL_OPENTELEMETRY_API_KEY]?.version; + if (!globalVersion || !isCompatible(globalVersion)) { return; } return _global[GLOBAL_OPENTELEMETRY_API_KEY]?.[type]; diff --git a/api/src/internal/semver.ts b/api/src/internal/semver.ts index e808df7b0fe..f766a7a38be 100644 --- a/api/src/internal/semver.ts +++ b/api/src/internal/semver.ts @@ -23,21 +23,27 @@ const re = /^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/; * * 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 + * - Major versions must match exactly + * - 1.x package cannot use global 2.x package + * - 2.x package cannot use global 1.x package + * - The minor version of the API module requesting access to the global API must be less than or equal to the minor version of this API + * - 1.3 package may use 1.4 global because the later global contains all functions 1.3 expects + * - 1.4 package may NOT use 1.3 global because it may try to call functions which don't exist on 1.3 + * - If the major version is 0, the minor version is treated as the major and the patch is treated as the minor * - 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 { +): (globalVersion: 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'); + // we cannot guarantee compatibility so we always return noop + return () => false; } const ownVersionParsed = { @@ -46,57 +52,59 @@ export function _makeCompatibilityCheck( patch: +myVersionMatch[3], }; - return function isCompatible(version: string): boolean { - if (acceptedVersions.has(version)) { + function _reject(v: string) { + rejectedVersions.add(v); + return false; + } + + function _accept(v: string) { + acceptedVersions.add(v); + return true; + } + + return function isCompatible(globalVersion: string): boolean { + if (acceptedVersions.has(globalVersion)) { return true; } - if (rejectedVersions.has(version)) { + if (rejectedVersions.has(globalVersion)) { return false; } - const m = version.match(re); - if (!m) { + const globalVersionMatch = globalVersion.match(re); + if (!globalVersionMatch) { // cannot parse other version - rejectedVersions.add(version); - return false; + // we cannot guarantee compatibility so we always noop + return _reject(globalVersion); } - const otherVersionParsed = { - major: +m[1], - minor: +m[2], - patch: +m[3], + const globalVersionParsed = { + major: +globalVersionMatch[1], + minor: +globalVersionMatch[2], + patch: +globalVersionMatch[3], }; // major versions must match - if (ownVersionParsed.major !== otherVersionParsed.major) { - rejectedVersions.add(version); - return false; + if (ownVersionParsed.major !== globalVersionParsed.major) { + return _reject(globalVersion); } - // 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; + if ( + ownVersionParsed.minor === globalVersionParsed.minor && + ownVersionParsed.patch <= globalVersionParsed.patch + ) { + return _accept(globalVersion); } - acceptedVersions.add(version); - return true; + return _reject(globalVersion); } - if (ownVersionParsed.minor < otherVersionParsed.minor) { - rejectedVersions.add(version); - return false; + if (ownVersionParsed.minor <= globalVersionParsed.minor) { + return _accept(globalVersion); } - acceptedVersions.add(version); - return true; + return _reject(globalVersion); }; } @@ -104,8 +112,13 @@ export function _makeCompatibilityCheck( * 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 + * - Major versions must match exactly + * - 1.x package cannot use global 2.x package + * - 2.x package cannot use global 1.x package + * - The minor version of the API module requesting access to the global API must be less than or equal to the minor version of this API + * - 1.3 package may use 1.4 global because the later global contains all functions 1.3 expects + * - 1.4 package may NOT use 1.3 global because it may try to call functions which don't exist on 1.3 + * - If the major version is 0, the minor version is treated as the major and the patch is treated as the minor * - Patch and build tag differences are not considered at this time * * @param version version of the API requesting an instance of the global API diff --git a/api/test/internal/semver.test.ts b/api/test/internal/semver.test.ts index 6020723b5cd..14e84b05c53 100644 --- a/api/test/internal/semver.test.ts +++ b/api/test/internal/semver.test.ts @@ -21,81 +21,113 @@ import { } from '../../src/internal/semver'; import { VERSION } from '../../src/version'; -describe('Version Compatibility', () => { +describe('semver', () => { 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'); - }); + it('returns false if own version cannot be parsed', () => { + const check = _makeCompatibilityCheck('this is not semver'); + assert.ok(!check('1.0.0')); }); - describe('incompatible if other version cannot be parsed', () => { + it('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('>= 1.0.0', () => { + const globalVersion = '5.5.5'; + const vers: [string, boolean][] = [ + // same major/minor run should be compatible + ['5.5.5', true], + ['5.5.6', true], + ['5.5.4', true], + + // if our version has a minor version increase, we may try to call methods which don't exist on the global + ['5.6.5', false], + ['5.6.6', false], + ['5.6.4', false], + + // if the global version is ahead of us by a minor revision, it has at least the methods we know about + ['5.4.5', true], + ['5.4.6', true], + ['5.4.4', true], + + // major version mismatch is always incompatible + ['6.5.5', false], + ['6.5.6', false], + ['6.5.4', false], + ['6.6.5', false], + ['6.6.6', false], + ['6.6.4', false], + ['6.4.5', false], + ['6.4.6', false], + ['6.4.4', false], + ['4.5.5', false], + ['4.5.6', false], + ['4.5.4', false], + ['4.6.5', false], + ['4.6.6', false], + ['4.6.4', false], + ['4.4.5', false], + ['4.4.6', false], + ['4.4.4', false], + ]; + + test(globalVersion, vers); }); - 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')); - }); + describe('< 1.0.0', () => { + const globalVersion = '0.5.5'; + const vers: [string, boolean][] = [ + // same minor/patch should be compatible + ['0.5.5', true], + + // if our version has a patch version increase, we may try to call methods which don't exist on the global + ['0.5.6', false], + + // if the global version is ahead of us by a patch revision, it has at least the methods we know about + ['0.5.4', true], + + // minor version mismatch is always incompatible + ['0.6.5', false], + ['0.6.6', false], + ['0.6.4', false], + ['0.4.5', false], + ['0.4.6', false], + ['0.4.4', false], + + // major version mismatch is always incompatible + ['1.5.5', false], + ['1.5.6', false], + ['1.5.4', false], + ['1.6.5', false], + ['1.6.6', false], + ['1.6.4', false], + ['1.4.5', false], + ['1.4.6', false], + ['1.4.4', false], + ]; + + test(globalVersion, vers); }); }); + +function test(globalVersion: string, vers: [string, boolean][]) { + describe(`global version is ${globalVersion}`, () => { + for (const [version, compatible] of vers) { + const alphaVersion = `${version}-alpha.1`; + it(`API version ${version} ${ + compatible ? 'should' : 'should not' + } be able to access global`, () => { + const check = _makeCompatibilityCheck(version); + assert.strictEqual(check(globalVersion), compatible); + + // alpha tag should have no effect different than the regular version + const alphaCheck = _makeCompatibilityCheck(alphaVersion); + assert.strictEqual(alphaCheck(globalVersion), compatible); + }); + } + }); +}