From b094707324ea7c555e79465c6d5a020c159eafa6 Mon Sep 17 00:00:00 2001 From: Felix Wiedenbach Date: Sat, 2 Oct 2021 14:08:16 +0200 Subject: [PATCH] remove mocking from implementation and use jest to mock git cli instead --- .../default/updatenotification/git_helper.js | 78 ++---- .../updatenotification_spec.js.snap | 45 ++++ .../unit/functions/updatenotification_spec.js | 231 +++++++++--------- 3 files changed, 191 insertions(+), 163 deletions(-) create mode 100644 tests/unit/functions/__snapshots__/updatenotification_spec.js.snap diff --git a/modules/default/updatenotification/git_helper.js b/modules/default/updatenotification/git_helper.js index 992a055b..b42b7d5c 100644 --- a/modules/default/updatenotification/git_helper.js +++ b/modules/default/updatenotification/git_helper.js @@ -12,7 +12,7 @@ class GitHelper { } getRefRegex(branch) { - return new RegExp("s*([a-z,0-9]+[.][.][a-z,0-9]+) " + branch, "g"); + return new RegExp(`s*([a-z,0-9]+[.][.][a-z,0-9]+) ${branch}`, "g"); } async execShell(command) { @@ -22,10 +22,10 @@ class GitHelper { } async isGitRepo(moduleFolder) { - const res = await this.execShell(`cd ${moduleFolder} && git remote -v`); + const { stderr } = await this.execShell(`cd ${moduleFolder} && git remote -v`); - if (res.stderr) { - Log.error(`Failed to fetch git data for ${moduleFolder}: ${res.stderr}`); + if (stderr) { + Log.error(`Failed to fetch git data for ${moduleFolder}: ${stderr}`); return false; } @@ -68,33 +68,27 @@ class GitHelper { isBehindInStatus: false }; - let res; if (repo.module === "default") { // the hash is only needed for the mm repo - res = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`); + const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`); - if (res.stderr) { - Log.error(`Failed to get current commit hash for ${repo.module}: ${res.stderr}`); + if (stderr) { + Log.error(`Failed to get current commit hash for ${repo.module}: ${stderr}`); } - gitInfo.hash = res.stdout; + gitInfo.hash = stdout; } - if (repo.res) { - // mocking - res = repo.res; - } else { - res = await this.execShell(`cd ${repo.folder} && git status -sb`); - } + const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git status -sb`); - if (res.stderr) { - Log.error(`Failed to get git status for ${repo.module}: ${res.stderr}`); + if (stderr) { + Log.error(`Failed to get git status for ${repo.module}: ${stderr}`); // exit without git status info return; } // only the first line of stdout is evaluated - let status = res.stdout.split("\n")[0]; + let status = stdout.split("\n")[0]; // examples for status: // ## develop...origin/develop // ## master...origin/master [behind 8] @@ -119,14 +113,7 @@ class GitHelper { } async getRepoInfo(repo) { - let gitInfo; - - if (repo.gitInfo) { - // mocking - gitInfo = repo.gitInfo; - } else { - gitInfo = await this.getStatusInfo(repo); - } + const gitInfo = await this.getStatusInfo(repo); if (!gitInfo) { return; @@ -136,20 +123,13 @@ class GitHelper { return gitInfo; } - let res; - - if (repo.res) { - // mocking - res = repo.res; - } else { - res = await this.execShell(`cd ${repo.folder} && git fetch --dry-run`); - } + const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch --dry-run`); // example output: // From https://github.com/MichMich/MagicMirror // e40ddd4..06389e3 develop -> origin/develop // here the result is in stderr (this is a git default, don't ask why ...) - const matches = res.stderr.match(this.getRefRegex(gitInfo.current)); + const matches = stderr.match(this.getRefRegex(gitInfo.current)); if (!matches || !matches[0]) { // no refs found, nothing to do @@ -158,8 +138,8 @@ class GitHelper { // get behind with refs try { - res = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`); - gitInfo.behind = parseInt(res.stdout); + const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`); + gitInfo.behind = parseInt(stdout); return gitInfo; } catch (err) { @@ -167,28 +147,18 @@ class GitHelper { } } - async getStatus() { - const gitResultList = []; - - for (let repo of this.gitRepos) { - const gitInfo = await this.getStatusInfo(repo); - - if (gitInfo) { - gitResultList.push(gitInfo); - } - } - - return gitResultList; - } - async getRepos() { const gitResultList = []; for (const repo of this.gitRepos) { - const gitInfo = await this.getRepoInfo(repo); + try { + const gitInfo = await this.getRepoInfo(repo); - if (gitInfo) { - gitResultList.push(gitInfo); + if (gitInfo) { + gitResultList.push(gitInfo); + } + } catch (e) { + Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`); } } diff --git a/tests/unit/functions/__snapshots__/updatenotification_spec.js.snap b/tests/unit/functions/__snapshots__/updatenotification_spec.js.snap new file mode 100644 index 00000000..fa5a000d --- /dev/null +++ b/tests/unit/functions/__snapshots__/updatenotification_spec.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Updatenotification custom module returns status information 1`] = ` +Object { + "behind": 7, + "current": "master", + "hash": "", + "isBehindInStatus": false, + "module": "MMM-Fuel", + "tracking": "origin/master", +} +`; + +exports[`Updatenotification custom module returns status information without hash 1`] = ` +Object { + "behind": 7, + "current": "master", + "hash": "", + "isBehindInStatus": false, + "module": "MMM-Fuel", + "tracking": "origin/master", +} +`; + +exports[`Updatenotification default returns status information 1`] = ` +Object { + "behind": 5, + "current": "develop", + "hash": "332e429a41f1a2339afd4f0ae96dd125da6beada", + "isBehindInStatus": false, + "module": "default", + "tracking": "origin/develop", +} +`; + +exports[`Updatenotification default returns status information early if isBehindInStatus 1`] = ` +Object { + "behind": 5, + "current": "develop", + "hash": "332e429a41f1a2339afd4f0ae96dd125da6beada", + "isBehindInStatus": true, + "module": "default", + "tracking": "origin/develop", +} +`; diff --git a/tests/unit/functions/updatenotification_spec.js b/tests/unit/functions/updatenotification_spec.js index f6214c1a..f5c5f396 100644 --- a/tests/unit/functions/updatenotification_spec.js +++ b/tests/unit/functions/updatenotification_spec.js @@ -1,126 +1,139 @@ -const path = require("path"); -const git_Helper = require("../../../modules/default/updatenotification/git_helper.js"); -const gitHelper = new git_Helper.gitHelper(); -gitHelper.add("default"); +jest.mock("util", () => ({ + ...jest.requireActual("util"), + promisify: jest.fn() +})); -const test1 = { - module: "test1", - folder: "", - res: { - stdout: "## master...origin/master [behind 8]", - stderr: "" - }, - gitInfo: { - module: "default", - // commits behind: - behind: 0, - // branch name: - current: "develop", - // current hash: - hash: "", - // remote branch: - tracking: "", - isBehindInStatus: false - } -}; +jest.mock("fs", () => ({ + ...jest.requireActual("fs"), + statSync: jest.fn() +})); -const test2 = { - module: "test2", - folder: "", - res: { - stdout: "## develop...origin/develop", - stderr: "" - } -}; - -const test3 = { - module: "test3", - folder: "", - res: { - stdout: "", - stderr: "error" - }, - gitInfo: { - module: "default", - // commits behind: - behind: 2, - // branch name: - current: "develop", - // current hash: - hash: "", - // remote branch: - tracking: "", - isBehindInStatus: true - } -}; - -const test4 = { - module: "default", - folder: path.join(__dirname, "../../.."), - res: { - stdout: "", - stderr: " e40ddd4..06389e3 develop -> origin/develop" - }, - gitInfo: { - module: "default", - // commits behind: - behind: 0, - // branch name: - current: "develop", - // current hash: - hash: "", - // remote branch: - tracking: "", - isBehindInStatus: false - } -}; +jest.mock("logger", () => ({ + ...jest.requireActual("logger"), + error: jest.fn(), + info: jest.fn() +})); describe("Updatenotification", function () { - it("should return valid output for git status", async function () { - const arr = await gitHelper.getStatus(); - expect(arr.length).toBe(1); - const gitInfo = arr[0]; - expect(gitInfo.current).not.toBe(""); - expect(gitInfo.hash).not.toBe(""); - }, 15000); + const execMock = jest.fn(); - it("should return behind=8 for test1", async function () { - const gitInfo = await gitHelper.getStatusInfo(test1); - expect(gitInfo.behind).toBe(8); - expect(gitInfo.isBehindInStatus).toBe(true); + let gitHelper; + + let gitRemoteOut; + let gitRevParseOut; + let gitStatusOut; + let gitFetchOut; + let gitRevListOut; + let gitRemoteErr; + let gitRevParseErr; + let gitStatusErr; + let gitFetchErr; + let gitRevListErr; + + beforeAll(async function () { + const { promisify } = require("util"); + promisify.mockReturnValue(execMock); + + const GitHelper = require(`../../../modules/default/updatenotification/git_helper`); + gitHelper = new GitHelper(); }); - it("should return behind=0 for test2", async function () { - const gitInfo = await gitHelper.getStatusInfo(test2); - expect(gitInfo.behind).toBe(0); - expect(gitInfo.isBehindInStatus).toBe(false); + beforeEach(function () { + gitRemoteOut = ""; + gitRevParseOut = ""; + gitStatusOut = ""; + gitFetchOut = ""; + gitRevListOut = ""; + gitRemoteErr = ""; + gitRevParseErr = ""; + gitStatusErr = ""; + gitFetchErr = ""; + gitRevListErr = ""; + + execMock.mockImplementation(function (command) { + if (command.includes("git remote -v")) { + return { stdout: gitRemoteOut, stderr: gitRemoteErr }; + } else if (command.includes("git rev-parse HEAD")) { + return { stdout: gitRevParseOut, stderr: gitRevParseErr }; + } else if (command.includes("git status -sb")) { + return { stdout: gitStatusOut, stderr: gitStatusErr }; + } else if (command.includes("git fetch --dry-run")) { + return { stdout: gitFetchOut, stderr: gitFetchErr }; + } else if (command.includes("git rev-list --ancestry-path --count")) { + return { stdout: gitRevListOut, stderr: gitRevListErr }; + } + }); }); - it("should return empty status object for test3", async function () { - const gitInfo = await gitHelper.getStatusInfo(test3); - expect(gitInfo).toBe(undefined); + afterEach(async function () { + gitHelper.gitRepos = []; + + jest.clearAllMocks(); }); - it("should return empty repo object for test2", async function () { - // no gitInfo provided in res, so returns undefined - const gitInfo = await gitHelper.getRepoInfo(test2); - expect(gitInfo).toBe(undefined); + describe("default", () => { + const moduleName = "default"; + + beforeEach(async function () { + gitRemoteOut = "origin\tgit@github.com:MichMich/MagicMirror.git (fetch)\norigin\tgit@github.com:MichMich/MagicMirror.git (push)\n"; + gitRevParseOut = "332e429a41f1a2339afd4f0ae96dd125da6beada"; + gitStatusOut = "## develop...origin/develop\n M tests/unit/functions/updatenotification_spec.js\n"; + gitFetchErr = "From github.com:MichMich/MagicMirror\n60e0377..332e429 develop -> origin/develop\n"; + gitRevListOut = "5"; + + await gitHelper.add(moduleName); + }); + + it("returns status information", async function () { + const repos = await gitHelper.getRepos(); + expect(repos[0]).toMatchSnapshot(); + expect(execMock).toHaveBeenCalledTimes(5); + }); + + it("returns status information early if isBehindInStatus", async function () { + gitStatusOut = "## develop...origin/develop [behind 5]"; + + const repos = await gitHelper.getRepos(); + expect(repos[0]).toMatchSnapshot(); + expect(execMock).toHaveBeenCalledTimes(3); + }); + + it("excludes repo if status can't be retrieved", async function () { + const errorMessage = "Failed to retrieve status"; + execMock.mockRejectedValueOnce(errorMessage); + + const repos = await gitHelper.getRepos(); + expect(repos.length).toBe(0); + + const { error } = require("logger"); + expect(error).toHaveBeenCalledWith(`Failed to retrieve repo info for ${moduleName}: Failed to retrieve status`); + }); + + it("excludes repo if refs don't match regex", async function () { + gitFetchErr = ""; + + const repos = await gitHelper.getRepos(); + expect(repos.length).toBe(0); + }); }); - it("should return empty repo object for test1", async function () { - // no regex match for refs in empty string, so returns undefined - const gitInfo = await gitHelper.getRepoInfo(test1); - expect(gitInfo).toBe(undefined); - }); + describe("custom module", () => { + const moduleName = "MMM-Fuel"; - it("should return empty repo object for test4", async function () { - // git ref list throws error, so returns undefined - const gitInfo = await gitHelper.getRepoInfo(test4); - expect(gitInfo).toBe(undefined); - }); + beforeEach(async function () { + gitRemoteOut = `origin\thttps://github.com/fewieden/${moduleName}.git (fetch)\norigin\thttps://github.com/fewieden/${moduleName}.git (push)\n`; + gitRevParseOut = "9d8310163da94441073a93cead711ba43e8888d0"; + gitStatusOut = "## master...origin/master"; + gitFetchErr = `From https://github.com/fewieden/${moduleName}\n19f7faf..9d83101 master -> origin/master`; + gitRevListOut = "7"; - it("should return behind=2 for test3", async function () { - const gitInfo = await gitHelper.getRepoInfo(test3); - expect(gitInfo.behind).toBe(2); + await gitHelper.add(moduleName); + }); + + it("returns status information without hash", async function () { + const repos = await gitHelper.getRepos(); + expect(repos[0]).toMatchSnapshot(); + expect(execMock).toHaveBeenCalledTimes(4); + }); }); });