From abb889b8a16b07cf169064d2bf1d7135be437dcc Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez-Leon Date: Thu, 11 Aug 2022 21:42:15 -0400 Subject: [PATCH] Allow marking of stubbed API Signed-off-by: Alvaro Sanchez-Leon --- src/comparator.ts | 33 ++++++++--- src/html-generator.ts | 18 ++++-- src/included.ts | 2 +- src/index.ts | 11 ++-- src/infos.ts | 29 +++++++++- src/stubbed-ranges.ts | 124 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 src/stubbed-ranges.ts diff --git a/src/comparator.ts b/src/comparator.ts index 6e0a4ec..fc71f86 100644 --- a/src/comparator.ts +++ b/src/comparator.ts @@ -11,16 +11,20 @@ import { ScannerEntry } from './scanner-entry'; import { Parser } from './parser'; import { DocEntry } from './doc-entry'; +import { Context, Infos, resolveStubbedInfo } from './infos'; +import { isInRange } from './stubbed-ranges'; export class Comparator { private globalResult: Map; - constructor(private readonly vsCodeEntries: ScannerEntry[], private readonly theiaEntries: ScannerEntry[]) { - + constructor( + private readonly vsCodeEntries: ScannerEntry[], + private readonly theiaEntries: ScannerEntry[], + private readonly infos: Infos + ) { // take latest vscode and then add value inside this.globalResult = new Map(); - } result(): Map { @@ -280,7 +284,9 @@ export class Comparator { } } else { // it's there, add it - docEntryLatestVsCodeCommand.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'yes' }); + const context = { namespaceKey: vsCodeNamespaceKey, element: docEntryLatestVsCodeCommand }; + const available = this.resolveAvailableYesOrStubbed(context, theiaEntry.version); + docEntryLatestVsCodeCommand.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available }); // now check members if (docEntryLatestVsCodeCommand.members && docEntryLatestVsCodeCommand.members.length > 0) { @@ -291,7 +297,9 @@ export class Comparator { // it's there, add it if (searchedMember) { - member.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'yes' }); + const memberContext = { ...context, subElement: member }; + const available = this.resolveAvailableYesOrStubbed(memberContext, theiaEntry.version); + member.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available }); } else { member.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'no' }); } @@ -307,7 +315,9 @@ export class Comparator { // it's there, add it if (searchedConstructor) { - constructor.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'yes' }); + const constructorContext = { ...context, subElement: constructor }; + const available = this.resolveAvailableYesOrStubbed(constructorContext, theiaEntry.version); + constructor.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available }); } else { constructor.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'no' }); } @@ -323,7 +333,9 @@ export class Comparator { // it's there, add it if (searchedUnion) { - union.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'yes' }); + const unionContext = { ...context, subElement: union }; + const available = this.resolveAvailableYesOrStubbed(unionContext, theiaEntry.version); + union.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available }); } else { union.includedIn.unshift({ version: `theia/${theiaEntry.version}`, available: 'no' }); } @@ -340,6 +352,13 @@ export class Comparator { } + private resolveAvailableYesOrStubbed(context: Context, version: string): 'stubbed' | 'yes' { + const stubbedInfo = resolveStubbedInfo(this.infos, context); + const isStubbed = stubbedInfo && isInRange(version, stubbedInfo); + + return isStubbed ? 'stubbed' : 'yes'; + } + /** * Removes all fully supported entries from the result. Entries that have unsupported children are not removed. */ diff --git a/src/html-generator.ts b/src/html-generator.ts index 953fcbc..f8e63d1 100644 --- a/src/html-generator.ts +++ b/src/html-generator.ts @@ -65,7 +65,9 @@ a { color: #0056b3; } row += this.generateNoteColumn(namespaceKey, command); row += ''; - if (command.constructors && command.constructors.length > 0 && command.includedIn[0].available === 'yes') { + const commandAvailable = command.includedIn[0].available; + const isCommandAvailable = commandAvailable === 'yes' || commandAvailable === 'stubbed'; + if (command.constructors && command.constructors.length > 0 && isCommandAvailable) { command.constructors.forEach(constructor => { let subRow = `
        constructor(${this.constructorPrettyName(constructor)})
`; @@ -79,7 +81,7 @@ a { color: #0056b3; } } // has members ? if yes then add lines for methods - if (command.members && command.members.length > 0 && command.includedIn[0].available === 'yes') { + if (command.members && command.members.length > 0 && isCommandAvailable) { command.members.forEach(member => { let subRow = `
        ${member.name}
`; @@ -93,7 +95,7 @@ a { color: #0056b3; } } // has unions ? if yes then add lines for methods - if (command.unions && command.unions.length > 0 && command.includedIn[0].available === 'yes') { + if (command.unions && command.unions.length > 0 && isCommandAvailable) { command.unions.forEach(union => { let subRow = `
        ${union.name}
`; @@ -158,12 +160,17 @@ a { color: #0056b3; } element.includedIn.forEach(included => { columns += '
'; columns += 'bg-success'; + } else if (included.available === 'stubbed') { + txt = ''; + columns += 'bg-warning'; + tooltip = tooltip + ' (stubbed)'; } else if (included.available === 'defined') { txt = ''; columns += 'bg-info'; @@ -172,7 +179,7 @@ a { color: #0056b3; } columns += 'bg-danger'; } - columns += `" style="max-width: 90px;" title="${included.version}">${txt}
`; + columns += `" style="max-width: 90px;" title="${tooltip}">${txt}
`; }); return columns; @@ -180,7 +187,6 @@ a { color: #0056b3; } private generateNoteColumn(namespace: string, element?: DocEntry, subElement?: DocEntry): string { const info = resolveInfo(this.infos, namespace, element?.name, subElement?.name); - const column = `
${info?._note ?? ''}
`; - return column; + return `
${info?._note ?? ''}
`; } } diff --git a/src/included.ts b/src/included.ts index a19aecf..bafeead 100644 --- a/src/included.ts +++ b/src/included.ts @@ -10,5 +10,5 @@ export interface Included { version: string; - available: 'yes' | 'defined' | 'no' | 'N/A'; + available: 'yes' | 'defined' | 'no' | 'N/A' | 'stubbed'; } diff --git a/src/index.ts b/src/index.ts index d270534..e4b0903 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,16 +23,17 @@ async function init() { const theiaEntries = await new GrabTheiaVersions().grab(); const vsCodeEntries = await new GrabVSCodeVersions().grab(); - // start comparator - const comparator = new Comparator(vsCodeEntries, theiaEntries); - comparator.init(); - comparator.compare(); - // Parse additional information console.log('⚙️ Parsing additional information from infos.yml...'); const infoFileContent = fs.readFileSync(path.resolve(__dirname, '..', 'conf', 'infos.yml'), 'utf-8'); const infos = parseInfos(infoFileContent); + // start comparator + const comparator = new Comparator(vsCodeEntries, theiaEntries, infos); + comparator.init(); + comparator.compare(); + + // Generate HTML output console.log('⚙️ Generating HTML report...'); const htmlGenerator = new HTMLGenerator(vsCodeEntries, theiaEntries, comparator.result(), infos); diff --git a/src/infos.ts b/src/infos.ts index 364dea8..d3722a6 100644 --- a/src/infos.ts +++ b/src/infos.ts @@ -9,15 +9,34 @@ **********************************************************************/ import * as YAML from 'yaml'; +import { DocEntry } from './doc-entry' + +export interface Context { + namespaceKey: string, + element?: DocEntry, + subElement?: DocEntry +} export interface Infos { [element: string]: Record; } + +export interface StubbedRange { + from: string, + to?: string +} + +interface StubbedInfo { + // Start with underscore to not conflict with sub element names + _stubbed?: StubbedRange +} + interface Note { // Start with underscore to not conflict with sub element names _note?: string; } -export type Info = Note & Infos; + +export type Info = Note & StubbedInfo & Infos; export function parseInfos(yaml: string): Infos { try { @@ -53,3 +72,11 @@ export function resolveInfo(infos: Infos, namespace: string, element?: string, s } return resolved; } + +export function resolveStubbedInfo(infos: Infos, context: Context): StubbedRange | undefined { + const info = resolveInfo(infos, context.namespaceKey, context.element?.name, context.subElement?.name); + if (!info?._stubbed) { + return undefined; + } + return info._stubbed +} \ No newline at end of file diff --git a/src/stubbed-ranges.ts b/src/stubbed-ranges.ts new file mode 100644 index 0000000..07b9723 --- /dev/null +++ b/src/stubbed-ranges.ts @@ -0,0 +1,124 @@ +// ***************************************************************************** +// Copyright (C) 2022 Ericsson and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +export interface VersionRange { + from: string, + to?: string +} + +const semverLength = 3; +export function isInRange(version: string, versionRange: VersionRange): boolean { + const { returning, semanticVersions } = resolveByBranchNames(version, versionRange); + if (returning !== undefined) { + return returning; + } + + return isVersionInRange(semanticVersions.input, semanticVersions.from, semanticVersions.to); +} + +function removeVersionDecorator(version: string) { + return version.startsWith('v') ? version.substring(1) : version; +} + +/** + * Handle provided versions by branch names; either 'master' or 'local' + */ +function resolveByBranchNames( + versionInput: string, + versionRange: VersionRange +): { returning?: boolean; semanticVersions?: { input: string; from: string; to: string } } { + const input = removeVersionDecorator(versionInput); + const from = removeVersionDecorator(versionRange.from); + let to = versionRange.to ? removeVersionDecorator(versionRange.to) : undefined; + + // a 'to' version pointing to 'master' or 'local' means there is no version ending a condition (stubbing) + // So we treat those as if the 'to' version was 'undefined' + to = to && (to === 'master' || to === 'local') ? undefined : to; + + if (input === 'master' || input == 'local') { + // Handling 'master' and 'local' which are considered the 'latest' + // Assuming, if 'to' version is available then 'latest' is no longer under the condition i.e. stubbed + return { returning: to ? false : true }; + } + + if (from === 'master' || from === 'local') { + // At this point input is not 'master' or 'local' i.e. input is referring to earlier versions than 'from' + return { returning: false }; + } + + // Done handling the supported branch names, + // returning strings representing semantic versions + return { semanticVersions: { input, from, to } } +} + +/** + * Each argument represents a semantic version + */ +function isVersionInRange(input: string, from: string, to?: string): boolean { + const inputArr = input.split('.'); + const fromArr = from.split('.'); + const toArr = to ? to.split('.') : undefined; + + if ( + inputArr.length != semverLength || + fromArr.length != semverLength || + !!(toArr && toArr.length != semverLength) + ) { + throw new Error('The expected length of semantic versions is 3'); + } + + const greaterOrEqual = isGreaterOrEqual(inputArr, fromArr); + if (!greaterOrEqual) { + return false; + } + + if (!toArr) { + // Not having a 'to' version means the condition is still present i.e. stubbing applies. + return true; + } + + return isLessOrEqual(inputArr, toArr); +} + +function isGreaterOrEqual(inputArr: string[], referenceArr: string[]): boolean { + for (let i = 0; i < semverLength; i++) { + if (+inputArr[i] > +referenceArr[i]) { + return true; + } + + if (+inputArr[i] < +referenceArr[i]) { + return false; + } + } + + // Must be equal + return true; +} + +function isLessOrEqual(inputArr: string[], referenceArr: string[]): boolean { + for (let i = 0; i < semverLength; i++) { + if (+inputArr[i] < +referenceArr[i]) { + return true; + } + + if (+inputArr[i] > +referenceArr[i]) { + return false; + } + } + + // Must be equal + return true; +}