Skip to content

Commit

Permalink
feat: add more useful output on batch actions, some minor fixes (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
oljekechoro authored Dec 9, 2020
1 parent f6da597 commit ecfd4a8
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 167 deletions.
3 changes: 2 additions & 1 deletion packages/cli-api/jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"<rootDir>/src/test/ts/**/*.ts"
],
"testPathIgnorePatterns": [
"/node_modules/"
"/node_modules/",
"<rootDir>/src/test/ts/utils.ts"
],
"moduleFileExtensions": [
"ts",
Expand Down
79 changes: 76 additions & 3 deletions packages/cli-api/src/main/ts/executors/deprecation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,88 @@
import { NpmRegClientWrapper, RegClient } from '@qiwi/npm-batch-client'
import { IDeprecatePackageParams, NpmRegClientWrapper, RegClient } from '@qiwi/npm-batch-client'

import { defaultRateLimit } from '../default'
import { TDeprecationConfig } from '../interfaces'
import { withRateLimit } from '../utils'

export const performDeprecation = async (config: TDeprecationConfig): Promise<any[]> => {
export const performDeprecation = async (config: TDeprecationConfig): Promise<void> => {
const regClient = new RegClient()
const batchClient = new NpmRegClientWrapper(
config.registryUrl,
config.auth,
withRateLimit<RegClient>(regClient, config.batch?.ratelimit || defaultRateLimit, ['deprecate'])
)
return batchClient.deprecateBatch(config.data, config.batch?.skipErrors)

return batchClient
.deprecateBatch(config.data, config.batch?.skipErrors)
.then(data => processResults(data, config))
}

export const processResults = (results: any[], config: TDeprecationConfig): void => {
const normalizedResults = config.batch?.skipErrors ? handleSettledResults(results) : results
const enrichedResults = enrichResults(normalizedResults, config.data)

printResults(
getSuccessfulResults(enrichedResults),
getFailedResults(enrichedResults),
config.batch?.jsonOutput
)
}

type TEnrichedBatchResult = {
result: any
packageInfo: IDeprecatePackageParams
}

export const enrichResults = (normalizedResults: any[], data: IDeprecatePackageParams[]): TEnrichedBatchResult[] =>
normalizedResults.map((result, i) => ({ result, packageInfo: data[i] }))

export const handleSettledResults = (results: PromiseSettledResult<any>[]): any[] =>
results.map(result => result.status === 'rejected'
? result.reason
: result.value
)

export const getSuccessfulResults = (
results: TEnrichedBatchResult[]
): IDeprecatePackageParams[] =>
results
.filter(item => item.result === null)
.map(item => item.packageInfo)

export const getFailedResults = (
results: TEnrichedBatchResult[]
): Array<IDeprecatePackageParams & { error: any }> =>
results
.filter(item => item.result !== null)
.map(item => ({ ...item.packageInfo, error: item.result.message || item.result }))

export const printResults = (
successfulPackages: Array<IDeprecatePackageParams>,
failedPackages: Array<IDeprecatePackageParams & { error: any }>,
jsonOutput?: boolean,
logger = console
): void => {
if (jsonOutput) {
logger.log(
JSON.stringify(
{
successfulPackages,
failedPackages
},
null, // eslint-disable-line unicorn/no-null
'\t'
)
)
return
}

if (successfulPackages.length > 0) {
logger.log('Following packages are deprecated successfully:')
logger.table(successfulPackages, ['packageName', 'version', 'message'])
}

if (failedPackages.length > 0) {
logger.error('Following packages are not deprecated due to errors:')
logger.table(failedPackages, ['packageName', 'version', 'message', 'error'])
}
}
3 changes: 2 additions & 1 deletion packages/cli-api/src/main/ts/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export interface IBaseConfig<T = any> {
batch?: {
ratelimit?: TRateLimit,
skipErrors?: boolean,
}
jsonOutput?: boolean
},
data: T,
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-api/src/main/ts/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { performDeprecation } from './executors'
import { ICliArgs } from './interfaces'
import { readFileToString, validateBaseConfig, validateDeprecationConfig } from './utils'

export const run = (configString: string): Promise<any[]> => {
export const run = (configString: string): Promise<void> => {
const rawConfig = JSON.parse(configString)
const validatedConfig = validateBaseConfig(rawConfig)
switch (validatedConfig.action) {
Expand All @@ -16,7 +16,7 @@ export const run = (configString: string): Promise<any[]> => {
}
}

export const readConfigAndRun = (args: ICliArgs): Promise<any[]> => {
export const readConfigAndRun = (args: ICliArgs): Promise<void> => {
const config = readFileToString(args.config)
return run(config)
}
214 changes: 167 additions & 47 deletions packages/cli-api/src/test/ts/executors/deprecation.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,70 @@
import nock from 'nock'

import { performDeprecation, TDeprecationConfig } from '../../../main/ts'
import {
getFailedResults,
handleSettledResults,
performDeprecation,
printResults,
processResults,
TDeprecationConfig
} from '../../../main/ts'
import * as deprecation from '../../../main/ts/executors/deprecation'
import { mockOutput } from '../utils'

describe('performDeprecation', () => {
it ('makes API requests with rate limit', async () => {
const registryUrl = 'http://localhost'
const config: TDeprecationConfig = {
registryUrl,
auth: {
username: 'username',
password: 'password',
},
batch: {
ratelimit: {
period: 500,
count: 2
}
},
action: 'deprecate',
data: [
{
packageName: 'foo',
version: '<1.2.0',
message: 'foo is deprecated',
},
{
packageName: 'bar',
version: '<1.3.0',
message: 'bar is deprecated',
},
{
packageName: 'baz',
version: '<1.4.0',
message: 'baz is deprecated',
},
{
packageName: 'bat',
version: '<1.5.0',
message: 'bat is deprecated',
},
]
const registryUrl = 'http://localhost'
const config: TDeprecationConfig = {
registryUrl,
auth: {
username: 'username',
password: 'password',
},
batch: {
ratelimit: {
period: 500,
count: 2
}
const mocks = config.data.map(data => ({
info: nock(registryUrl)
.get(`/${data.packageName}?write=true`)
.reply(200, { versions: { '0.1.0': {} }}),
deprecate: nock(registryUrl)
.put(`/${data.packageName}`)
.reply(200)
}))
},
action: 'deprecate',
data: [
{
packageName: 'foo',
version: '<1.2.0',
message: 'foo is deprecated',
},
{
packageName: 'bar',
version: '<1.3.0',
message: 'bar is deprecated',
},
{
packageName: 'baz',
version: '<1.4.0',
message: 'baz is deprecated',
},
{
packageName: 'bat',
version: '<1.5.0',
message: 'bat is deprecated',
},
]
}

const createMocks = () =>
config.data.map(data => ({
info: nock(registryUrl)
.get(`/${data.packageName}?write=true`)
.reply(200, { versions: { '0.1.0': {} } }),
deprecate: nock(registryUrl)
.put(`/${data.packageName}`)
.reply(200)
}))

describe('performDeprecation', () => {
beforeEach(() => jest.restoreAllMocks())

it('makes API requests with rate limit', async () => {
mockOutput()
const mocks = createMocks()
const startTime = Date.now()
await performDeprecation(config)
const endTime = Date.now() - startTime
Expand All @@ -57,3 +73,107 @@ describe('performDeprecation', () => {
expect(endTime).toBeGreaterThanOrEqual(500)
})
})

describe('processResults', () => {
it('calls necessary util functions', () => {
mockOutput()
const getSuccessfulResults = jest.spyOn(deprecation, 'getSuccessfulResults')
const getFailedResults = jest.spyOn(deprecation, 'getFailedResults')

processResults([], config)

expect(getSuccessfulResults).toHaveBeenCalled()
expect(getFailedResults).toHaveBeenCalled()
})

it('normalizes settled results', () => {
mockOutput()
const handleSettledResultsSpy = jest.spyOn(deprecation, 'handleSettledResults')

// eslint-disable-next-line unicorn/no-null
processResults([{ status: 'fulfilled', value: null }, { status: 'fulfilled', value: null }], {
...config,
batch: {
...config.batch,
skipErrors: true
}
})

expect(handleSettledResultsSpy).toHaveBeenCalled()
})
})

describe('printResults', function () {
it('prints successful results only when they are presented', () => {
const consoleMock = {
log: jest.fn(),
table: jest.fn(),
error: jest.fn()
}

const failedPackages: any[] = []
printResults(config.data, failedPackages, false, consoleMock as any)
expect(consoleMock.log).toHaveBeenCalledWith(expect.stringContaining('success'))
expect(consoleMock.table).toHaveBeenCalledWith(config.data, expect.any(Array))
expect(consoleMock.error).not.toHaveBeenCalledWith(expect.stringContaining('errors'))
expect(consoleMock.table).not.toHaveBeenCalledWith(failedPackages, expect.any(Array))
})

it('does not print successful results when they are not presented', () => {
const consoleMock = {
log: jest.fn(),
table: jest.fn(),
error: jest.fn()
}

const successfulPackages: any[] = []
const failedPackages = config.data.map(item => ({ ...item, error: 'error' }))
printResults(successfulPackages, failedPackages, false, consoleMock as any)
expect(consoleMock.log).not.toHaveBeenCalledWith(expect.stringContaining('success'))
expect(consoleMock.table).not.toHaveBeenCalledWith(successfulPackages, expect.any(Array))
expect(consoleMock.error).toHaveBeenCalledWith(expect.stringContaining('errors'))
expect(consoleMock.table).toHaveBeenCalledWith(failedPackages, expect.any(Array))
})

it('prints results in JSON when appropriate flag is presented', () => {
const consoleMock = {
log: jest.fn(),
table: jest.fn(),
error: jest.fn()
}
const successfulPackages: any[] = []
const failedPackages: any[] = []
printResults(successfulPackages, failedPackages, true, consoleMock as any)
expect(consoleMock.log).toHaveBeenCalledWith(JSON.stringify(
{ successfulPackages, failedPackages },
null, // eslint-disable-line unicorn/no-null
'\t'
))

expect(consoleMock.error).not.toHaveBeenCalled()
expect(consoleMock.table).not.toHaveBeenCalled()
})
})

describe('utils', () => {
test('getFailedResults handles different types of error', () => {
const data = getFailedResults(
config.data.map(
(packageInfo, i) => ({
packageInfo,
result: i % 2 === 0 ? 'error' : new Error('error')
}))
)
expect(data).toEqual(config.data.map(item => ({ ...item, error: 'error' })))
})

test('handleSettledResults normalizes fulfilled and rejected results', () => {
expect(handleSettledResults([
{ status: 'fulfilled', value: 'value' },
{ status: 'rejected', reason: 'reason' },
])).toEqual([
'value',
'reason'
])
})
});
5 changes: 5 additions & 0 deletions packages/cli-api/src/test/ts/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IBaseConfig, readConfigAndRun } from '../../main/ts'
import * as deprecation from '../../main/ts/executors/deprecation'
import * as utils from '../../main/ts/utils/misc'
import * as validators from '../../main/ts/utils/validators'
import { mockOutput } from './utils'

const config: IBaseConfig = {
registryUrl: 'http://localhost',
Expand All @@ -17,6 +18,7 @@ beforeEach(() => jest.restoreAllMocks())

describe('readConfigAndRun', () => {
it('reads and validates config', () => {
mockOutput()
const readSpy = jest.spyOn(utils, 'readFileToString')
.mockImplementation(() => JSON.stringify(config))
const baseValidatorSpy = jest.spyOn(validators, 'validateBaseConfig')
Expand All @@ -26,6 +28,7 @@ describe('readConfigAndRun', () => {
})

it('calls performDeprecation for deprecate', () => {
mockOutput()
jest.spyOn(utils, 'readFileToString')
.mockImplementation(() => JSON.stringify(config))
const spy = jest.spyOn(deprecation, 'performDeprecation')
Expand All @@ -34,6 +37,7 @@ describe('readConfigAndRun', () => {
})

it('calls performDeprecation for un-deprecate', () => {
mockOutput()
jest.spyOn(utils, 'readFileToString')
.mockImplementation(() => JSON.stringify({ ...config, action: 'un-deprecate' }))
const spy = jest.spyOn(deprecation, 'performDeprecation')
Expand All @@ -42,6 +46,7 @@ describe('readConfigAndRun', () => {
})

it('throws an error when action is not recognized', () => {
mockOutput()
jest.spyOn(utils, 'readFileToString')
.mockImplementation(() => JSON.stringify({ ...config, action: 'foo' }))
expect(() => readConfigAndRun({ config: 'foo' })).toThrow()
Expand Down
Loading

0 comments on commit ecfd4a8

Please sign in to comment.