Skip to content
This repository was archived by the owner on Apr 13, 2020. It is now read-only.

Commit 5706003

Browse files
dennisseahyradsmikhamandrebriggs
authored
[REFACTOR] project init command (#203)
* replace any with interface * correct the type definition for IVariableGroupDataVariable[] * [REFACTOR] making code compact * add test to create-variable-group command * breaking larger function in scaffold class into smaller one, so we can add tests * simplier commander interface * using type guard in hasValue function * incorporate Evan's feedback * fix jsdoc * fixed the problem where definition.yaml file is not created * [REFACTOR] commands/hld/init.ts and add tests 1. move the command decoration out to a JSON file 2. standardized on command decorator function name, initCommandDecorator -> commandDecorator 3. added support for having default values in command builder 4. added tests to boost the coverage from 59% to 89% 5. create helper functions in tests so test code is more manage-able * modified code to make sure that log is flushed before the command exit * adding missing Promise<void> return type * [REFACTOR] project init command 1. have command decoration to a separation JSON file 2. improve test coverage from 60 to 80%. 3. create helper functions for creating random dir 4. create helper functions for checking if file exists 5. remove unused function (ls) in init.ts 6. proper exit code for init.ts 7. make reduce function in init.ts more readable successfully ran validations.sh * have ringName = uuid() in test * fix typo * remove hld-lifecycle.yaml * add doc to test Co-authored-by: Yvonne Radsmikham <yvonne.radsmikham@gmail.com> Co-authored-by: Andre Briggs <andrebriggs@users.noreply.github.com>
1 parent b3546a2 commit 5706003

File tree

6 files changed

+206
-118
lines changed

6 files changed

+206
-118
lines changed

src/commands/project/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Command } from "../command";
22
import { commandDecorator as createVariableGroupCommandDecorator } from "./create-variable-group";
3-
import { initCommandDecorator } from "./init";
3+
import { commandDecorator as initCommandDecorator } from "./init";
44
import { deployLifecyclePipelineCommandDecorator } from "./pipeline";
55

66
export const projectCommand = Command(
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"command": "init",
3+
"alias": "i",
4+
"description": "Initialize your spk repository. Add starter bedrock.yaml, maintainers.yaml, hld-lifecycle.yaml, and .gitignore files to your project.",
5+
"options": [
6+
{
7+
"arg": "-r, --default-ring <branch-name>",
8+
"description": "Specify a default ring; this corresponds to a default branch which you wish to push initial revisions to",
9+
"required": false,
10+
"defaultValue": "master"
11+
}
12+
]
13+
}

src/commands/project/init.test.ts

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,36 @@
1-
import fs from "fs";
2-
import os from "os";
3-
import path from "path";
41
import uuid from "uuid/v4";
52
import { Bedrock, Maintainers, write } from "../../config";
3+
import { createTempDir, getMissingFilenames } from "../../lib/ioUtil";
64
import { IBedrockFile, IMaintainersFile } from "../../types";
7-
import { initialize } from "./init";
5+
import { execute, initialize } from "./init";
6+
import * as init from "./init";
87

9-
/**
10-
* Helper to create a new random directory to initialize
11-
*/
12-
const createNewProject = () => {
13-
// Create random directory to initialize
14-
const randomTmpDir = path.join(os.tmpdir(), uuid());
15-
fs.mkdirSync(randomTmpDir);
16-
return randomTmpDir;
17-
};
8+
const CREATED_FILES = [
9+
".gitignore",
10+
"bedrock.yaml",
11+
"maintainers.yaml",
12+
"hld-lifecycle.yaml"
13+
];
1814

1915
describe("Initializing a blank/new bedrock repository", () => {
2016
test("all standard files get generated in the project root on init", async () => {
21-
// init
22-
const randomTmpDir = createNewProject();
17+
const randomTmpDir = createTempDir();
2318
await initialize(randomTmpDir);
2419

2520
// bedrock.yaml, maintainers.yaml should be in a the root for a 'standard' project
26-
const filepathsShouldExist = [
27-
".gitignore",
28-
"bedrock.yaml",
29-
"maintainers.yaml",
30-
"hld-lifecycle.yaml"
31-
].map(filename => path.join(randomTmpDir, filename));
32-
33-
for (const filepath of filepathsShouldExist) {
34-
expect(fs.existsSync(filepath)).toBe(true);
35-
}
21+
const missing = getMissingFilenames(randomTmpDir, CREATED_FILES);
22+
expect(missing.length).toBe(0); // no files are missing hence length 0
3623

3724
// ensure service specific files do not get created
38-
const filepathsShouldNotExist = [
25+
const unexpected = getMissingFilenames(randomTmpDir, [
3926
"Dockerfile",
4027
"azure-pipelines.yaml"
41-
].map(filename => path.join(randomTmpDir, filename));
42-
for (const filepath of filepathsShouldNotExist) {
43-
expect(fs.existsSync(filepath)).toBe(false);
44-
}
28+
]);
29+
expect(unexpected.length).toBe(2);
4530
});
4631

4732
test("defaultRings gets injected successfully", async () => {
48-
const randomTmpDir = createNewProject();
33+
const randomTmpDir = createTempDir();
4934
const ringName = uuid();
5035
await initialize(randomTmpDir, { defaultRing: ringName });
5136
const bedrock = Bedrock(randomTmpDir);
@@ -55,7 +40,7 @@ describe("Initializing a blank/new bedrock repository", () => {
5540

5641
describe("initializing an existing file does not modify it", () => {
5742
test("bedrock.yaml does not get modified", async () => {
58-
const randomDir = createNewProject();
43+
const randomDir = createTempDir();
5944
const bedrockFile: IBedrockFile = {
6045
rings: { master: { isDefault: true } },
6146
services: {
@@ -80,7 +65,7 @@ describe("initializing an existing file does not modify it", () => {
8065
});
8166

8267
test("maintainers.yaml does not get modified", async () => {
83-
const randomDir = createNewProject();
68+
const randomDir = createTempDir();
8469
const maintainersFile: IMaintainersFile = {
8570
services: {
8671
"some/random/dir": {
@@ -96,3 +81,34 @@ describe("initializing an existing file does not modify it", () => {
9681
expect(updatedMaintainers).toStrictEqual(maintainersFile);
9782
});
9883
});
84+
85+
describe("Test execute function", () => {
86+
it("positive test", async () => {
87+
jest.spyOn(init, "initialize");
88+
const exitFn = jest.fn();
89+
await execute(
90+
{
91+
defaultRing: "master"
92+
},
93+
exitFn
94+
);
95+
expect(exitFn).toBeCalledTimes(1);
96+
expect(initialize).toBeCalledTimes(1);
97+
expect(exitFn.mock.calls).toEqual([[0]]); // 0: success
98+
});
99+
100+
it("negative test", async () => {
101+
jest.spyOn(init, "initialize").mockImplementation(() => {
102+
throw new Error();
103+
});
104+
const exitFn = jest.fn();
105+
await execute(
106+
{
107+
defaultRing: "master"
108+
},
109+
exitFn
110+
);
111+
expect(exitFn).toBeCalledTimes(1);
112+
expect(exitFn.mock.calls).toEqual([[1]]); // 1: error
113+
});
114+
});

src/commands/project/init.ts

Lines changed: 52 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,54 @@
11
import commander from "commander";
22
import fs from "fs";
33
import path from "path";
4-
import shelljs from "shelljs";
54
import { Bedrock, write } from "../../config";
5+
import { build as buildCmd, exit as exitCmd } from "../../lib/commandBuilder";
66
import {
77
generateGitIgnoreFile,
88
generateHldLifecyclePipelineYaml
99
} from "../../lib/fileutils";
1010
import { exec } from "../../lib/shell";
1111
import { logger } from "../../logger";
1212
import { IBedrockFile, IHelmConfig, IMaintainersFile } from "../../types";
13+
import decorator from "./init.decorator.json";
1314

14-
/**
15-
* Adds the init command to the commander command object
16-
*
17-
* @param command Commander command object to decorate
18-
*/
19-
export const initCommandDecorator = (command: commander.Command): void => {
20-
command
21-
.command("init")
22-
.alias("i")
23-
.description(
24-
"Initialize your spk repository. Will add starter bedrock.yaml, maintainers.yaml, hld-lifecycle.yaml, and .gitignore files to your project."
25-
)
26-
.option(
27-
"-r, --default-ring <branch-name>",
28-
"Specify a default ring; this corresponds to a default branch which you wish to push initial revisions to",
29-
"master"
30-
)
31-
.action(async opts => {
32-
const { defaultRing } = opts;
33-
const projectPath = process.cwd();
15+
// values that we need to pull out from command operator
16+
interface ICommandOptions {
17+
defaultRing: string;
18+
}
3419

35-
try {
36-
let bedrockFile: IBedrockFile | undefined;
37-
try {
38-
bedrockFile = Bedrock();
39-
} catch (err) {
40-
logger.info(err);
41-
}
20+
export const execute = async (
21+
opts: ICommandOptions,
22+
exitFn: (status: number) => Promise<void>
23+
) => {
24+
// defaultRing shall always have value (not undefined nor null)
25+
// because it has default value as "master"
26+
const defaultRing = opts.defaultRing;
27+
const projectPath = process.cwd();
28+
29+
try {
30+
const _ = Bedrock(); // TOFIX: Is this to check if Bedrock config exist?
31+
} catch (err) {
32+
logger.info(err);
33+
}
4234

43-
// Type check all parsed command line args here.
44-
if (typeof defaultRing !== "string") {
45-
throw new Error(
46-
`--default-ring must be of type 'string', '${defaultRing}' of type '${typeof defaultRing}' given`
47-
);
48-
}
35+
try {
36+
await initialize(projectPath, { defaultRing });
37+
await exitFn(0);
38+
} catch (err) {
39+
logger.error(`Error occurred while initializing project ${projectPath}`);
40+
logger.error(err);
41+
await exitFn(1);
42+
}
43+
};
4944

50-
await initialize(projectPath, {
51-
defaultRing
52-
});
53-
} catch (err) {
54-
logger.error(
55-
`Error occurred while initializing project ${projectPath}`
56-
);
57-
logger.error(err);
58-
}
45+
export const commandDecorator = (command: commander.Command): void => {
46+
buildCmd(command, decorator).action(async (opts: ICommandOptions) => {
47+
await execute(opts, async (status: number) => {
48+
await exitCmd(logger);
49+
process.exit(status);
5950
});
51+
});
6052
};
6153

6254
/**
@@ -70,47 +62,21 @@ export const initCommandDecorator = (command: commander.Command): void => {
7062
*/
7163
export const initialize = async (
7264
rootProjectPath: string,
73-
opts?: {
74-
defaultRing?: string;
75-
}
65+
opts?: ICommandOptions
7666
) => {
77-
const { defaultRing } = opts || {};
7867
const absProjectRoot = path.resolve(rootProjectPath);
7968
logger.info(`Initializing project Bedrock project ${absProjectRoot}`);
8069

70+
const defaultRing = opts ? [opts.defaultRing] : [];
8171
// Initialize all paths
82-
await generateBedrockFile(
83-
absProjectRoot,
84-
[],
85-
defaultRing ? [defaultRing] : []
86-
);
87-
await generateMaintainersFile(absProjectRoot, []);
72+
await generateBedrockFile(absProjectRoot, [], defaultRing);
73+
await generateMaintainersFile(absProjectRoot, []); // TOFIX: packagePaths is hardcoded to []
8874
await generateHldLifecyclePipelineYaml(absProjectRoot);
8975
generateGitIgnoreFile(absProjectRoot, "spk.log");
9076

9177
logger.info(`Project initialization complete!`);
9278
};
9379

94-
/**
95-
* Helper function for listing files/dirs in a path
96-
*
97-
* @param dir path-like string; what you would pass to ls in bash
98-
*/
99-
const ls = async (dir: string): Promise<string[]> => {
100-
const lsRet = shelljs.ls(dir);
101-
if (lsRet.code !== 0) {
102-
logger.error(lsRet.stderr);
103-
throw new Error(
104-
`Error listing files in ${dir}; Ensure this directory exists or specify a different one with the --packages-dir option.`
105-
);
106-
}
107-
108-
// Returned object includes piping functions as well; strings represent the actual output of the function
109-
const filesAndDirectories = lsRet.filter(out => typeof out === "string");
110-
111-
return filesAndDirectories;
112-
};
113-
11480
/**
11581
* Writes out a default maintainers.yaml file
11682
*
@@ -199,8 +165,19 @@ const generateBedrockFile = async (
199165
const absPackagePaths = packagePaths.map(p => path.resolve(p));
200166
logger.info(`Generating bedrock.yaml file in ${absProjectPath}`);
201167

168+
const base: IBedrockFile = {
169+
rings: defaultRings.reduce<{ [ring: string]: { isDefault: boolean } }>(
170+
(defaults, ring) => {
171+
defaults[ring] = { isDefault: true };
172+
return defaults;
173+
},
174+
{}
175+
),
176+
services: {}
177+
};
178+
202179
// Populate bedrock file
203-
const bedrockFile: IBedrockFile = absPackagePaths.reduce<IBedrockFile>(
180+
const bedrockFile = absPackagePaths.reduce<IBedrockFile>(
204181
(file, absPackagePath) => {
205182
const relPathToPackageFromRoot = path.relative(
206183
absProjectPath,
@@ -221,16 +198,7 @@ const generateBedrockFile = async (
221198
};
222199
return file;
223200
},
224-
{
225-
rings: defaultRings.reduce<{ [ring: string]: { isDefault: boolean } }>(
226-
(defaults, ring) => {
227-
defaults[ring] = { isDefault: true };
228-
return defaults;
229-
},
230-
{}
231-
),
232-
services: {}
233-
}
201+
base
234202
);
235203

236204
// Check if a bedrock.yaml already exists; skip write if present

src/lib/ioUtil.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import { createTempDir, getMissingFilenames } from "./ioUtil";
4+
5+
describe("test createTempDir function", () => {
6+
it("create and existence check", () => {
7+
const name = createTempDir();
8+
expect(fs.existsSync(name)).toBe(true);
9+
fs.rmdirSync(name);
10+
expect(fs.existsSync(name)).toBe(false);
11+
});
12+
});
13+
14+
describe("test doFilesExist function", () => {
15+
it("all files exists", () => {
16+
const dir = createTempDir();
17+
const files = ["hello", "world"];
18+
19+
files.forEach(f => {
20+
fs.writeFileSync(path.join(dir, `${f}.txt`), f);
21+
});
22+
23+
const missing = getMissingFilenames(
24+
dir,
25+
files.map(f => `${f}.txt`)
26+
);
27+
expect(missing.length).toBe(0);
28+
});
29+
it("none of files exists", () => {
30+
const dir = createTempDir();
31+
const files = ["hello", "world"];
32+
33+
const missing = getMissingFilenames(
34+
dir,
35+
files.map(f => `${f}.txt`)
36+
);
37+
expect(missing.length).toBe(2);
38+
});
39+
it("some of files exists", () => {
40+
const dir = createTempDir();
41+
const files = ["hello", "world", "again"];
42+
43+
files
44+
.filter(f => f !== "again")
45+
.forEach(f => {
46+
fs.writeFileSync(path.join(dir, `${f}.txt`), f);
47+
});
48+
49+
const missing = getMissingFilenames(
50+
dir,
51+
files.map(f => `${f}.txt`)
52+
);
53+
expect(missing.length).toBe(1);
54+
});
55+
});

0 commit comments

Comments
 (0)