Skip to content

Commit

Permalink
feat: Add all resolvers unit tests #12
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr committed Mar 26, 2020
1 parent f781972 commit 6bff702
Show file tree
Hide file tree
Showing 15 changed files with 327 additions and 53 deletions.
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"eslint-plugin-prefer-arrow": "^1.1.7",
"eslint-plugin-prettier": "^3.1.2",
"generate-changelog": "^1.8.0",
"husky": "^4.2.3",
"jest": "^25.1.0",
"jest-serial-runner": "^1.1.0",
"lint-staged": "^10.0.9",
Expand Down Expand Up @@ -123,6 +124,13 @@
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
],
"rules": {
"subject-case": [
2,
"never",
["start-case", "pascal-case", "upper-case"]
]
}
}
}
14 changes: 11 additions & 3 deletions src/resolvers/ciResolver/impl/ciResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ import { injectable, multiInject } from 'inversify';
import { ISpecificCIResolver } from '../interfaces/ISpecificCIResolver';
import { ITargetMatcher } from '../interfaces/ITargetMatcher';
import { IResolverResult } from '../../types';
import { ILoggerFactory } from '../../../utils/logger';
import { ILogger } from '../../../utils/logger/interfaces/logger';

@injectable()
export class CiResolver extends ICIResolver {
private readonly logger: ILogger;

constructor(
@multiInject(ISpecificCIResolver) private resolvers: ISpecificCIResolver[],
private targetMatcher: ITargetMatcher
@multiInject(ISpecificCIResolver) private readonly resolvers: ISpecificCIResolver[],
private readonly targetMatcher: ITargetMatcher,
loggerFactory: ILoggerFactory
) {
super();
this.logger = loggerFactory.getLogger(`Ci Resolver`);
}

async resolve({ repoPath, targetNode, packageReleaseDate }: ICIResolveOptions): Promise<IResolverResult> {
this.logger.debug(`Checking if repo in ${repoPath} has a ci that uses node ${targetNode}`);
for (const resolver of this.resolvers) {
let nodeVersions: string[] | undefined;
try {
nodeVersions = await resolver.resolve({
const resolverResult = await resolver.resolve({
repoPath,
});
nodeVersions = resolverResult.nodeVersions;
} catch (e) {
console.log(`Failed to find node versions in resolver ${resolver.resolverName} due to an unknown error`, e);
}
Expand Down
59 changes: 46 additions & 13 deletions src/resolvers/ciResolver/impl/resolvers/appveyor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import * as path from 'path';
import { ISpecificCIResolverOptions, ISpecificCIResolver } from '../../interfaces/ISpecificCIResolver';
import {
ISpecificCIResolverOptions,
ISpecificCIResolver,
ISpecificCIResolverResponse,
} from '../../interfaces/specificCIResolver';
import { inject, injectable } from 'inversify';
import { FS, TYPES } from '../../../../container/nodeModulesContainer';
import { parse } from 'yaml';
import { ILoggerFactory } from '../../../../utils/logger';
import { ILogger } from '../../../../utils/logger/interfaces/logger';

const nodeVersionRegex = /node ([^\s]+)/i;
const nodeVersionInstallRegex = /Install-Product node ([^\s]+)/i;
Expand Down Expand Up @@ -51,45 +57,72 @@ const parseStack = (stack: string | undefined, versions: string[]): void => {
const ciFileName = `.appveyor.yml`;
const resolverName = `appVeyor`;

const getCiFileName = (repoPath: string): string => {
return path.join(repoPath, ciFileName);
};

@injectable()
export class AppVeyorResolver extends ISpecificCIResolver {
public readonly resolverName = resolverName;
private readonly logger: ILogger;

constructor(@inject(TYPES.FS) private fs: FS) {
constructor(@inject(TYPES.FS) private fs: FS, loggerFactory: ILoggerFactory) {
super();
this.logger = loggerFactory.getLogger(`AppVeyor Resolver`);
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<string[] | undefined> {
const fileName = path.join(repoPath, ciFileName);
let fileContents: string;
public async isRelevant({ repoPath }: ISpecificCIResolverOptions): Promise<boolean> {
const fileName = getCiFileName(repoPath);
this.logger.debug(`Checking if ${fileName} exists and readable`);
try {
fileContents = await this.fs.promises.readFile(fileName, `utf-8`);
} catch (e) {
return;
await this.fs.promises.access(fileName, this.fs.constants.R_OK);
this.logger.debug(`File ${fileName} exists and readable`);
return true;
} catch (err) {
this.logger.debug(`File ${fileName} is not readable`, err);
return false;
}
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<ISpecificCIResolverResponse> {
const fileName = getCiFileName(repoPath);
const fileContents = await this.fs.promises.readFile(fileName, `utf-8`);
const versions: string[] = [];
const yaml = parse(fileContents);
const stack = yaml.stack;
parseStack(stack, versions);
const installCommands: string[] = yaml.install;
if (!installCommands) {
return versions;
return {
nodeVersions: versions,
};
}
const nodeVersion = installCommands.map(psObjectMapper).map(nodeVersionInstallMapper).filter(emptyFilter)[0];
if (!nodeVersion) {
return versions;
return {
nodeVersions: versions,
};
}
const match = nodeEnvRegex.exec(nodeVersion);
if (!match) {
versions.push(nodeVersion);
return versions;
return {
nodeVersions: versions,
};
} else {
const matrixVarName = match[1];
const environment: any[] = yaml.environment?.matrix || yaml.environment;
if (!environment) {
return versions;
return {
nodeVersions: versions,
};
}
return versions.concat(environment.map(envObjectMapper(matrixVarName)).filter(emptyFilter) as string[]);
const nodeVersions = versions.concat(
environment.map(envObjectMapper(matrixVarName)).filter(emptyFilter) as string[]
);
return {
nodeVersions,
};
}
}
}
42 changes: 32 additions & 10 deletions src/resolvers/ciResolver/impl/resolvers/circle.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,51 @@
import * as path from 'path';
import { ISpecificCIResolverOptions, ISpecificCIResolver } from '../../interfaces/ISpecificCIResolver';
import {
ISpecificCIResolverOptions,
ISpecificCIResolver,
ISpecificCIResolverResponse,
} from '../../interfaces/specificCIResolver';
import { inject, injectable } from 'inversify';
import { FS, TYPES } from '../../../../container/nodeModulesContainer';
import { ILoggerFactory } from '../../../../utils/logger';
import { ILogger } from '../../../../utils/logger/interfaces/logger';

const nodeVersionRegex = new RegExp(`image: node:(\\d+)`, `ig`);
const nodeVersionRegex2 = new RegExp(`image: \.+?\/node:(\\d+)`, `ig`);

const ciFilePath = path.join(`.circleci`, `config.yml`);
const resolverName = `circleCi`;

const getCiFileName = (repoPath: string): string => {
return path.join(repoPath, ciFilePath);
};

@injectable()
export class CircleCiResolver extends ISpecificCIResolver {
public readonly resolverName = resolverName;
private readonly logger: ILogger;

constructor(@inject(TYPES.FS) private fs: FS) {
constructor(@inject(TYPES.FS) private fs: FS, loggerFactory: ILoggerFactory) {
super();
this.logger = loggerFactory.getLogger(`Circle CI Resolver`);
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<string[] | undefined> {
const fileName = path.join(repoPath, ciFilePath);
const versions = new Set<string>();
let fileContents: string;
public async isRelevant({ repoPath }: ISpecificCIResolverOptions): Promise<boolean> {
const fileName = getCiFileName(repoPath);
this.logger.debug(`Checking if ${fileName} exists and readable`);
try {
fileContents = await this.fs.promises.readFile(fileName, `utf-8`);
} catch (e) {
return;
await this.fs.promises.access(fileName, this.fs.constants.R_OK);
this.logger.debug(`File ${fileName} exists and readable`);
return true;
} catch (err) {
this.logger.debug(`File ${fileName} is not readable`, err);
return false;
}
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<ISpecificCIResolverResponse> {
const versions = new Set<string>();
const fileName = getCiFileName(repoPath);
const fileContents = await this.fs.promises.readFile(fileName, `utf-8`);
let match: RegExpExecArray | null;
do {
match = nodeVersionRegex.exec(fileContents);
Expand All @@ -39,6 +59,8 @@ export class CircleCiResolver extends ISpecificCIResolver {
versions.add(match[1]);
}
} while (match);
return Array.from(versions);
return {
nodeVersions: Array.from(versions),
};
}
}
47 changes: 36 additions & 11 deletions src/resolvers/ciResolver/impl/resolvers/github.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
import * as path from 'path';
import { ISpecificCIResolverOptions, ISpecificCIResolver } from '../../interfaces/ISpecificCIResolver';
import {
ISpecificCIResolverOptions,
ISpecificCIResolver,
ISpecificCIResolverResponse,
} from '../../interfaces/specificCIResolver';
import { inject, injectable } from 'inversify';
import { FS, TYPES } from '../../../../container/nodeModulesContainer';
import { ILoggerFactory } from '../../../../utils/logger';
import { ILogger } from '../../../../utils/logger/interfaces/logger';

const nodeVersionRegex = new RegExp(`node-version:\\s*(.+)`, `ig`);

const ciFilePath = path.join(`.github`, `workflows`);
const ciFilesPath = path.join(`.github`, `workflows`);
const resolverName = `githubActions`;

const getCiFilesPath = (repoPath: string): string => {
return path.join(repoPath, ciFilesPath);
};

@injectable()
export class GithubActionsResolver extends ISpecificCIResolver {
public readonly resolverName = resolverName;
private readonly logger: ILogger;

constructor(@inject(TYPES.FS) private fs: FS) {
constructor(@inject(TYPES.FS) private fs: FS, loggerFactory: ILoggerFactory) {
super();
this.logger = loggerFactory.getLogger(`Github Actions Resolver`);
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<string[] | undefined> {
const folderName = path.join(repoPath, ciFilePath);
const foundVersions = new Set<string>();
let files: string[];
public async isRelevant({ repoPath }: ISpecificCIResolverOptions): Promise<boolean> {
const folderName = getCiFilesPath(repoPath);
this.logger.debug(`Checking if ${folderName} exists and readable`);
try {
files = await this.fs.promises.readdir(folderName);
} catch (e) {
return;
await this.fs.promises.access(folderName, this.fs.constants.R_OK);
this.logger.debug(`File ${folderName} exists and readable. Checking if is is a directory`);
const stat = await this.fs.promises.stat(folderName);
const isDirectory = stat.isDirectory();
this.logger.debug(`File ${folderName} is directory? ${isDirectory}`);
return isDirectory;
} catch (err) {
this.logger.debug(`File ${folderName} is not readable`, err);
return false;
}
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<ISpecificCIResolverResponse> {
const folderName = getCiFilesPath(repoPath);
const foundVersions = new Set<string>();
const files = await this.fs.promises.readdir(folderName);
for (const fileName of files) {
const fileContents = await this.fs.promises.readFile(path.join(folderName, fileName), `utf-8`);
let match: RegExpExecArray | null;
Expand All @@ -47,6 +70,8 @@ export class GithubActionsResolver extends ISpecificCIResolver {
}
} while (match);
}
return Array.from(foundVersions);
return {
nodeVersions: Array.from(foundVersions),
};
}
}
37 changes: 29 additions & 8 deletions src/resolvers/ciResolver/impl/resolvers/travis.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import * as path from 'path';
import { ISpecificCIResolverOptions, ISpecificCIResolver, LTS_VERSION } from '../../interfaces/ISpecificCIResolver';
import {
ISpecificCIResolverOptions,
ISpecificCIResolver,
LTS_VERSION,
ISpecificCIResolverResponse,
} from '../../interfaces/specificCIResolver';
import { inject, injectable } from 'inversify';
import { FS, TYPES } from '../../../../container/nodeModulesContainer';
import { parse } from 'yaml';
import { ILoggerFactory } from '../../../../utils/logger';
import { ILogger } from '../../../../utils/logger/interfaces/logger';

const ciFileName = `.travis.yml`;
const resolverName = `travisCi`;
Expand All @@ -14,22 +21,36 @@ const ltsMapper = (nodeVersion: string): string => {
return nodeVersion;
};

const getCiFileName = (repoPath: string): string => {
return path.join(repoPath, ciFileName);
};

@injectable()
export class TravisCiResolver extends ISpecificCIResolver {
public readonly resolverName = resolverName;
private readonly logger: ILogger;

constructor(@inject(TYPES.FS) private fs: FS) {
constructor(@inject(TYPES.FS) private fs: FS, loggerFactory: ILoggerFactory) {
super();
this.logger = loggerFactory.getLogger(`Travis CI Resolver`);
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<string[] | undefined> {
const fileName = path.join(repoPath, ciFileName);
let fileContents: string;
public async isRelevant({ repoPath }: ISpecificCIResolverOptions): Promise<boolean> {
const fileName = getCiFileName(repoPath);
this.logger.debug(`Checking if ${fileName} exists and readable`);
try {
fileContents = await this.fs.promises.readFile(fileName, `utf-8`);
} catch (e) {
return;
await this.fs.promises.access(fileName, this.fs.constants.R_OK);
this.logger.debug(`File ${fileName} exists and readable`);
return true;
} catch (err) {
this.logger.debug(`File ${fileName} is not readable`, err);
return false;
}
}

public async resolve({ repoPath }: ISpecificCIResolverOptions): Promise<ISpecificCIResolverResponse> {
const fileName = getCiFileName(repoPath);
const fileContents = await this.fs.promises.readFile(fileName, `utf-8`);
const yaml = parse(fileContents);
const versions = yaml.node_js;
return versions.map(ltsMapper);
Expand Down
7 changes: 6 additions & 1 deletion src/resolvers/ciResolver/interfaces/ISpecificCIResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ export interface ISpecificCIResolverOptions {
repoPath: string;
}

export interface ISpecificCIResolverResponse {
nodeVersions?: string[];
}

export const LTS_VERSION = `LTS_VERSION`;

@injectable()
export abstract class ISpecificCIResolver {
public abstract async resolve(options: ISpecificCIResolverOptions): Promise<string[] | undefined>;
public abstract async isRelevant(options: ISpecificCIResolverOptions): Promise<boolean>;
public abstract async resolve(options: ISpecificCIResolverOptions): Promise<ISpecificCIResolverResponse>;
public abstract readonly resolverName: string;
}
Loading

0 comments on commit 6bff702

Please sign in to comment.