diff --git a/.eslintrc.js b/.eslintrc.js index cbd5bb85f..8af41bfce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,7 +19,11 @@ module.exports = { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "error", - { varsIgnorePattern: "_" }, + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, ], "@typescript-eslint/no-non-null-assertion": "error", "guard-for-in": "error", diff --git a/package.json b/package.json index b5ec20a0d..3e4decb1c 100644 --- a/package.json +++ b/package.json @@ -978,6 +978,7 @@ "compile": "tsc -p ./", "clean": "rimraf out/", "watch": "tsc -watch -p ./", + "test-unit": "ts-mocha src/*.test.ts", "test": "ts-mocha src/test/unit/*.test.ts", "test-extension": "rimraf out/ && tsc -p ./ && node out/test/extension/runTest.js", "build": "yarn clean && vsce package --yarn", @@ -994,6 +995,7 @@ "@types/node": "18.7.14", "@types/remarkable": "^2.0.3", "@types/semver": "^7.3.12", + "@types/sinon": "^10.0.13", "@types/vscode": "1.59.0", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", @@ -1007,6 +1009,7 @@ "ovsx": "0.5.1", "prettier": "2.7.1", "rimraf": "^3.0.2", + "sinon": "^14.0.0", "ts-mocha": "^10.0.0", "typescript": "4.8.2", "vsce": "2.11.0" diff --git a/src/ConfigurationTarget.ts b/src/ConfigurationTarget.ts new file mode 100644 index 000000000..fbadd7a73 --- /dev/null +++ b/src/ConfigurationTarget.ts @@ -0,0 +1,4 @@ +export enum ConfigurationTarget { + Global, + Workspace, +} diff --git a/src/getServerVersion.ts b/src/getServerVersion.ts index a1ca2ddfd..0bf7aabc9 100644 --- a/src/getServerVersion.ts +++ b/src/getServerVersion.ts @@ -1,21 +1,21 @@ import { commands, - ConfigurationTarget, ExtensionContext, window, WorkspaceConfiguration, + ConfigurationTarget as VConfigurationTarget, } from "vscode"; import * as metalsLanguageClient from "metals-languageclient"; import * as workbenchCommands from "./workbenchCommands"; import http from "https"; import { getConfigValue } from "./util"; +import { DefaultCheckForUpdateRepo } from "./repository/CheckForUpdateRepo"; +import { needCheckForUpdates } from "./service/checkForUpdate"; +import { ConfigurationTarget } from "./ConfigurationTarget"; const serverVersionSection = "serverVersion"; const suggestLatestUpgrade = "suggestLatestUpgrade"; -const currentVersionKey = "currentVersion"; -const lastUpdatedAtKey = "lastUpdatedAt"; - export function getServerVersion( config: WorkspaceConfiguration, context: ExtensionContext @@ -40,12 +40,15 @@ async function validateCurrentVersion( suggestLatestUpgrade ); + const checkForUpdateRepo = new DefaultCheckForUpdateRepo(context); + const checkForUpdate = async () => { if (suggestUpgradeSetting?.value) { return needCheckForUpdates( serverVersion, - suggestUpgradeSetting.target, - context + todayString(), + fromVSCode(suggestUpgradeSetting.target), + checkForUpdateRepo ); } else { return false; @@ -68,13 +71,17 @@ async function validateCurrentVersion( nextVersion, suggestUpgradeSetting.target ); - saveVersionDate(nextVersion, suggestUpgradeSetting.target, context); + checkForUpdateRepo.saveLastUpdated( + nextVersion, + todayString(), + fromVSCode(suggestUpgradeSetting.target) + ); } else if (result == ignoreChoice) { // extend the current version expiration date - saveVersionDate( + checkForUpdateRepo.saveLastUpdated( serverVersion, - suggestUpgradeSetting.target, - context + todayString(), + fromVSCode(suggestUpgradeSetting.target) ); } }); @@ -101,46 +108,6 @@ async function fetchLatest(): Promise { return sorted[sorted.length - 1]; } -/** - * The logic is the following: - * - if version was set more than a day ago - update is needed - * - if version is seen in a first time (user changed version in config by it self) - the update will be delayed for a day - */ -async function needCheckForUpdates( - currentVersion: string, - target: ConfigurationTarget, - context: ExtensionContext -): Promise { - const state = - target === ConfigurationTarget.Global - ? context.globalState - : context.workspaceState; - const prevVersion = state.get(currentVersionKey); - const lastUpdated = state.get(lastUpdatedAtKey); - - const today = todayString(); - if (prevVersion !== currentVersion) { - saveVersionDate(currentVersion, target, context); - return false; - } else { - return lastUpdated !== today; - } -} - -function saveVersionDate( - version: string, - target: ConfigurationTarget, - context: ExtensionContext -): void { - const state = - target === ConfigurationTarget.Global - ? context.globalState - : context.workspaceState; - - state.update(currentVersionKey, version); - state.update(lastUpdatedAtKey, todayString()); -} - function warnIfIsOutdated(config: WorkspaceConfiguration): void { metalsLanguageClient.checkServerVersion({ config, @@ -175,10 +142,24 @@ function warnIfIsOutdated(config: WorkspaceConfiguration): void { }); } -function todayString(): string { +/** + * @returns YYYY-MM-DD in a local date + */ +export function todayString(): string { const date = new Date(); const year = date.getFullYear().toString(); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const day = date.getDate().toString().padStart(2, "0"); return [year, month, day].join("-"); } + +function fromVSCode(target: VConfigurationTarget): ConfigurationTarget { + switch (target) { + case VConfigurationTarget.Global: + return ConfigurationTarget.Global; + case VConfigurationTarget.Workspace: + return ConfigurationTarget.Workspace; + case VConfigurationTarget.WorkspaceFolder: + return ConfigurationTarget.Workspace; + } +} diff --git a/src/repository/CheckForUpdateRepo.ts b/src/repository/CheckForUpdateRepo.ts new file mode 100644 index 000000000..f5db29b24 --- /dev/null +++ b/src/repository/CheckForUpdateRepo.ts @@ -0,0 +1,50 @@ +import { ExtensionContext, Memento } from "vscode"; +import { ConfigurationTarget } from "../ConfigurationTarget"; + +type LastUpdated = { + prevVersion?: string; + lastUpdatedAt?: string; +}; + +export interface CheckForUpdateRepo { + getLastUpdated(target: ConfigurationTarget): LastUpdated; + saveLastUpdated( + serverVersion: string, + lastUpdatedAt: string, + target: ConfigurationTarget + ): void; +} + +export class DefaultCheckForUpdateRepo implements CheckForUpdateRepo { + constructor(private context: ExtensionContext) {} + + private CurrentVersionKey = "currentVersion"; + private LastUpdatedAtKey = "lastUpdatedAt"; + + getLastUpdated(target: ConfigurationTarget): LastUpdated { + const state = this.storage(target); + const prevVersion = state.get(this.CurrentVersionKey); + const lastUpdatedAt = state.get(this.LastUpdatedAtKey); + return { + prevVersion, + lastUpdatedAt, + }; + } + + saveLastUpdated( + serverVersion: string, + lastUpdatedAt: string, + target: ConfigurationTarget + ): void { + const state = this.storage(target); + state.update(this.CurrentVersionKey, serverVersion); + state.update(this.LastUpdatedAtKey, lastUpdatedAt); + return; + } + + private storage(target: ConfigurationTarget): Memento { + return target === ConfigurationTarget.Global + ? this.context.globalState + : this.context.workspaceState; + } +} diff --git a/src/service/checkForUpdate.ts b/src/service/checkForUpdate.ts new file mode 100644 index 000000000..f1137b92e --- /dev/null +++ b/src/service/checkForUpdate.ts @@ -0,0 +1,23 @@ +import { ConfigurationTarget } from "../ConfigurationTarget"; +import { CheckForUpdateRepo } from "../repository/CheckForUpdateRepo"; + +/** + * The logic is the following: + * - if version was set more than a day ago - update is needed + * - if version is seen for the first time (user changed version in config by it self) - the update will be delayed for a day + */ +export async function needCheckForUpdates( + currentVersion: string, + today: string, + target: ConfigurationTarget, + repo: CheckForUpdateRepo +): Promise { + const { prevVersion, lastUpdatedAt } = repo.getLastUpdated(target); + + if (prevVersion !== currentVersion) { + repo.saveLastUpdated(currentVersion, today, target); + return false; + } else { + return lastUpdatedAt !== today; + } +} diff --git a/src/test/unit/checkForUpdate.test.ts b/src/test/unit/checkForUpdate.test.ts new file mode 100644 index 000000000..02080d82a --- /dev/null +++ b/src/test/unit/checkForUpdate.test.ts @@ -0,0 +1,89 @@ +import assert from "assert"; +import { ConfigurationTarget } from "../../ConfigurationTarget"; +import { needCheckForUpdates } from "../../service/checkForUpdate"; +import { CheckForUpdateRepo } from "../../repository/CheckForUpdateRepo"; +import sinon from "sinon"; + +class MockRepo implements CheckForUpdateRepo { + constructor(private prevVersion?: string, private lastUpdatedAt?: string) {} + getLastUpdated(_target: ConfigurationTarget): { + prevVersion?: string | undefined; + lastUpdatedAt?: string | undefined; + } { + return { + prevVersion: this.prevVersion, + lastUpdatedAt: this.lastUpdatedAt, + }; + } + saveLastUpdated( + _serverVersion: string, + _lastUpdatedAt: string, + _target: ConfigurationTarget + ): void { + return; + } +} + +describe("needCheckForUpdates", () => { + it("should false if nothing has saved / save current versions", async () => { + const currentVersion = "0.11.8"; + const today = "2022-01-01"; + const repo = new MockRepo(undefined, undefined); + const spy = sinon.spy(repo, "saveLastUpdated"); + const actual = await needCheckForUpdates( + currentVersion, + today, + ConfigurationTarget.Global, + repo + ); + assert(actual === false); + assert(spy.calledWith(currentVersion, today, ConfigurationTarget.Global)); + }); + + it("should false if currentVersion was seen for the first time / save current versions", async () => { + const prevVersion = "0.11.8"; + const currentVersion = "0.11.9"; + const today = "2022-01-01"; + const repo = new MockRepo(prevVersion, today); + const spy = sinon.spy(repo, "saveLastUpdated"); + const actual = await needCheckForUpdates( + currentVersion, + today, + ConfigurationTarget.Global, + repo + ); + assert(actual === false); + assert(spy.calledWith(currentVersion, today, ConfigurationTarget.Global)); + }); + + it("should false if currentVersion is set today", async () => { + const currentVersion = "0.11.8"; + const today = "2022-01-01"; + const repo = new MockRepo(currentVersion, today); + const spy = sinon.spy(repo, "saveLastUpdated"); + const actual = await needCheckForUpdates( + currentVersion, + today, + ConfigurationTarget.Global, + repo + ); + assert(actual === false); + assert(spy.notCalled); + }); + + it("should true if currentVersion is set more than a day ago", async () => { + const currentVersion = "0.11.8"; + const lastUpdatedAt = "2022-01-01"; + const today = "2022-01-02"; + const repo = new MockRepo(currentVersion, lastUpdatedAt); + const spy = sinon.spy(repo, "saveLastUpdated"); + const actual = await needCheckForUpdates( + currentVersion, + today, + ConfigurationTarget.Global, + repo + ); + assert(actual === true); + assert(spy.notCalled); + }); +}); diff --git a/yarn.lock b/yarn.lock index a0f09cd54..330525f53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -62,6 +62,34 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@>=5", "@sinonjs/fake-timers@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/samsam@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.1.tgz#627f7f4cbdb56e6419fa2c1a3e4751ce4f6a00b1" + integrity sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA== + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@sourcegraph/scip-typescript@^0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@sourcegraph/scip-typescript/-/scip-typescript-0.2.9.tgz#17365c803bcd9f68c3be570f13263955e277a9e9" @@ -129,6 +157,18 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c" integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== +"@types/sinon@^10.0.13": + version "10.0.13" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.13.tgz#60a7a87a70d9372d0b7b38cc03e825f46981fb83" + integrity sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e" + integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== + "@types/vscode@1.59.0": version "1.59.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.59.0.tgz#11c93f5016926126bf30b47b9ece3bd617eeef31" @@ -692,6 +732,11 @@ diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -1308,6 +1353,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1342,6 +1392,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + keytar@^7.7.0: version "7.7.0" resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2" @@ -1382,6 +1437,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -1554,6 +1614,17 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +nise@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.1.tgz#ac4237e0d785ecfcb83e20f389185975da5c31f3" + integrity sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A== + dependencies: + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" ">=5" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + node-abi@^2.21.0: version "2.30.1" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" @@ -1699,6 +1770,13 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -1986,6 +2064,18 @@ simple-get@^3.0.3: once "^1.3.1" simple-concat "^1.0.0" +sinon@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.0.tgz#203731c116d3a2d58dc4e3cbe1f443ba9382a031" + integrity sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw== + dependencies: + "@sinonjs/commons" "^1.8.3" + "@sinonjs/fake-timers" "^9.1.2" + "@sinonjs/samsam" "^6.1.1" + diff "^5.0.0" + nise "^5.1.1" + supports-color "^7.2.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -2084,7 +2174,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -2210,6 +2300,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-detect@4.0.8, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"