mirror of
https://github.com/theoludwig/programming-challenges.git
synced 2024-12-08 00:45:29 +01:00
test(cli): add commands/generate/challenge
This commit is contained in:
parent
88acd8cfef
commit
d14fd0b62a
117
cli/commands/generate/__test__/challenge.test.ts
Normal file
117
cli/commands/generate/__test__/challenge.test.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { PassThrough } from 'node:stream'
|
||||||
|
import path from 'node:path'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import getStream from 'get-stream'
|
||||||
|
import fsMock from 'mock-fs'
|
||||||
|
import date from 'date-and-time'
|
||||||
|
|
||||||
|
import { cli } from '../../../cli'
|
||||||
|
import { isExistingPath } from '../../../utils/isExistingPath'
|
||||||
|
|
||||||
|
const input = ['generate', 'challenge']
|
||||||
|
const githubUser = 'Divlo'
|
||||||
|
const challengeName = 'aaaa-test-jest'
|
||||||
|
const inputChallengeName = `--challenge=${challengeName}`
|
||||||
|
const inputGitHubUser = `--github-user=${githubUser}`
|
||||||
|
|
||||||
|
describe('programming-challenges generate challenge', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fsMock(
|
||||||
|
{
|
||||||
|
[process.cwd()]: fsMock.load(process.cwd(), { recursive: true })
|
||||||
|
},
|
||||||
|
{ createCwd: false }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fsMock.restore()
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeds and generate the new challenge', async () => {
|
||||||
|
console.log = jest.fn()
|
||||||
|
const dateString = date.format(new Date(), 'D MMMM Y', true)
|
||||||
|
const stream = new PassThrough()
|
||||||
|
const exitCode = await cli.run(
|
||||||
|
[...input, inputChallengeName, inputGitHubUser],
|
||||||
|
{
|
||||||
|
stdin: process.stdin,
|
||||||
|
stdout: stream,
|
||||||
|
stderr: stream
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stream.end()
|
||||||
|
expect(exitCode).toEqual(0)
|
||||||
|
const challengePath = path.join(process.cwd(), 'challenges', challengeName)
|
||||||
|
const readmePath = path.join(challengePath, 'README.md')
|
||||||
|
const readmeContent = await fs.promises.readFile(readmePath, { encoding: 'utf-8' })
|
||||||
|
const successMessage = `${chalk.bold.green('Success:')} created the new challenge at ${challengePath}.`
|
||||||
|
expect(console.log).toHaveBeenCalledWith(successMessage)
|
||||||
|
expect(await isExistingPath(challengePath)).toBeTruthy()
|
||||||
|
expect(readmeContent).toMatch(`# ${challengeName}
|
||||||
|
|
||||||
|
Created by [@${githubUser}](https://github.com/${githubUser}) on ${dateString}.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Description of the challenge...
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the \`test\` folder for examples of input/output.
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails without options', async () => {
|
||||||
|
const stream = new PassThrough()
|
||||||
|
const promise = getStream(stream)
|
||||||
|
const exitCode = await cli.run(input, {
|
||||||
|
stdin: process.stdin,
|
||||||
|
stdout: stream,
|
||||||
|
stderr: stream
|
||||||
|
})
|
||||||
|
stream.end()
|
||||||
|
expect(exitCode).toEqual(1)
|
||||||
|
const output = await promise
|
||||||
|
expect(output).toContain('Unknown Syntax Error')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails with already existing challenge', async () => {
|
||||||
|
console.error = jest.fn()
|
||||||
|
const stream = new PassThrough()
|
||||||
|
const exitCode = await cli.run(
|
||||||
|
[...input, '--challenge=hello-world', inputGitHubUser],
|
||||||
|
{
|
||||||
|
stdin: process.stdin,
|
||||||
|
stdout: stream,
|
||||||
|
stderr: stream
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(console.error).toHaveBeenCalledWith(
|
||||||
|
`${chalk.bold.red('Error:')} The challenge already exists: hello-world.`
|
||||||
|
)
|
||||||
|
stream.end()
|
||||||
|
expect(exitCode).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fails with invalid challenge name', async () => {
|
||||||
|
console.error = jest.fn()
|
||||||
|
const stream = new PassThrough()
|
||||||
|
const exitCode = await cli.run(
|
||||||
|
[...input, '--challenge=hEllO-world', inputGitHubUser],
|
||||||
|
{
|
||||||
|
stdin: process.stdin,
|
||||||
|
stdout: stream,
|
||||||
|
stderr: stream
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stream.end()
|
||||||
|
expect(exitCode).toEqual(1)
|
||||||
|
expect(console.error).toHaveBeenCalledWith(
|
||||||
|
`${chalk.bold.red('Error:')} Invalid challenge name.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
@ -78,7 +78,7 @@ export class RunTestCommand extends Command {
|
|||||||
programmingLanguageName: this.programmingLanguage
|
programmingLanguageName: this.programmingLanguage
|
||||||
})
|
})
|
||||||
await solution.test()
|
await solution.test()
|
||||||
console.log(Test.successMessage)
|
console.log(Test.SUCCESS_MESSAGE)
|
||||||
return 0
|
return 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import execa from 'execa'
|
import execa from 'execa'
|
||||||
import ora from 'ora'
|
import ora from 'ora'
|
||||||
|
|
||||||
const CONTAINER_TAG = 'programming-challenges'
|
|
||||||
|
|
||||||
class Docker {
|
class Docker {
|
||||||
|
static CONTAINER_TAG = 'programming-challenges'
|
||||||
|
|
||||||
public async build (): Promise<void> {
|
public async build (): Promise<void> {
|
||||||
const loader = ora('Building the Docker image').start()
|
const loader = ora('Building the Docker image').start()
|
||||||
try {
|
try {
|
||||||
await execa.command(`docker build --tag=${CONTAINER_TAG} ./`)
|
await execa.command(`docker build --tag=${Docker.CONTAINER_TAG} ./`)
|
||||||
loader.stop()
|
loader.stop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loader.fail()
|
loader.fail()
|
||||||
@ -17,7 +17,7 @@ class Docker {
|
|||||||
|
|
||||||
public async run (input: string): Promise<string> {
|
public async run (input: string): Promise<string> {
|
||||||
const subprocess = execa.command(
|
const subprocess = execa.command(
|
||||||
`docker run --interactive --rm ${CONTAINER_TAG}`,
|
`docker run --interactive --rm ${Docker.CONTAINER_TAG}`,
|
||||||
{
|
{
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,11 @@ import execa from 'execa'
|
|||||||
import { Challenge } from './Challenge'
|
import { Challenge } from './Challenge'
|
||||||
import { Solution } from './Solution'
|
import { Solution } from './Solution'
|
||||||
|
|
||||||
const solutionsRegex = new RegExp(
|
const solutionsRegex = /challenges\/[\s\S]*\/solutions\/(c|cpp|cs|dart|java|javascript|python|rust|typescript)\/[\s\S]*\/(.*).(c|cpp|cs|dart|java|js|py|rs|ts)/
|
||||||
/challenges\/[\s\S]*\/solutions\/(c|cpp|cs|dart|java|javascript|python|rust|typescript)\/[\s\S]*\/(.*).(c|cpp|cs|dart|java|js|py|rs|ts)/
|
|
||||||
)
|
|
||||||
|
|
||||||
const dockerRegex = new RegExp(
|
const dockerRegex = /templates\/docker\/(c|cpp|cs|dart|java|javascript|python|rust|typescript)\/Dockerfile/
|
||||||
/templates\/docker\/(c|cpp|cs|dart|java|javascript|python|rust|typescript)\/Dockerfile/
|
|
||||||
)
|
|
||||||
|
|
||||||
const inputOutputRegex = new RegExp(
|
const inputOutputRegex = /challenges\/[\s\S]*\/test\/(.*)\/(input.txt|output.txt)/
|
||||||
/challenges\/[\s\S]*\/test\/(.*)\/(input.txt|output.txt)/
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface GitAffectedOptions {
|
export interface GitAffectedOptions {
|
||||||
isContinuousIntegration: boolean
|
isContinuousIntegration: boolean
|
||||||
@ -89,31 +83,17 @@ export class GitAffected implements GitAffectedOptions {
|
|||||||
const affectedInputOutput = files.filter((filePath) => {
|
const affectedInputOutput = files.filter((filePath) => {
|
||||||
return inputOutputRegex.test(filePath)
|
return inputOutputRegex.test(filePath)
|
||||||
})
|
})
|
||||||
const affectedChallenges = affectedInputOutput.map((filePath) => {
|
const affectedChallengesFromInputOutput = affectedInputOutput.map((filePath) => {
|
||||||
const [, challengeName] = filePath.replaceAll('\\', '/').split('/')
|
const [, challengeName] = filePath.replaceAll('\\', '/').split('/')
|
||||||
return new Challenge({ name: challengeName })
|
return new Challenge({ name: challengeName })
|
||||||
})
|
})
|
||||||
const solutionsChallenges = await Solution.getManyByPaths(affectedSolutionsPaths)
|
const solutionsChallenges = await Solution.getManyByPaths(affectedSolutionsPaths)
|
||||||
const solutionsDocker = await Solution.getManyByProgrammingLanguages(affectedLanguages)
|
const solutionsDocker = await Solution.getManyByProgrammingLanguages(affectedLanguages)
|
||||||
const solutions: Solution[] = solutionsDocker
|
const solutions: Solution[] = [...solutionsDocker, ...solutionsChallenges]
|
||||||
for (const solution of solutionsChallenges) {
|
for (const challenge of affectedChallengesFromInputOutput) {
|
||||||
if (!affectedLanguages.includes(solution.programmingLanguageName)) {
|
|
||||||
solutions.push(solution)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const challenge of affectedChallenges) {
|
|
||||||
let isSolutionIncluded = false
|
|
||||||
for (const solution of solutions) {
|
|
||||||
if (solution.challenge.name === challenge.name) {
|
|
||||||
isSolutionIncluded = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isSolutionIncluded) {
|
|
||||||
const solutionsByChallenge = await Solution.getManyByChallenge(challenge)
|
const solutionsByChallenge = await Solution.getManyByChallenge(challenge)
|
||||||
solutions.push(...solutionsByChallenge)
|
solutions.push(...solutionsByChallenge)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const solutionsUnique: Solution[] = []
|
const solutionsUnique: Solution[] = []
|
||||||
for (const solution of solutions) {
|
for (const solution of solutions) {
|
||||||
const isAlreadyIncluded = solutionsUnique.some((solutionUnique) => {
|
const isAlreadyIncluded = solutionsUnique.some((solutionUnique) => {
|
||||||
|
@ -37,7 +37,7 @@ export class Test implements TestOptions {
|
|||||||
public output: string
|
public output: string
|
||||||
public stdout: string
|
public stdout: string
|
||||||
public elapsedTimeMilliseconds: number
|
public elapsedTimeMilliseconds: number
|
||||||
static successMessage = `${chalk.bold.green('Success:')} Tests passed! 🎉`
|
static SUCCESS_MESSAGE = `${chalk.bold.green('Success:')} Tests passed! 🎉`
|
||||||
|
|
||||||
constructor (options: TestOptions) {
|
constructor (options: TestOptions) {
|
||||||
this.index = options.index
|
this.index = options.index
|
||||||
@ -99,15 +99,15 @@ export class Test implements TestOptions {
|
|||||||
const name = `${solution.challenge.name}/${solution.programmingLanguageName}/${solution.name}`
|
const name = `${solution.challenge.name}/${solution.programmingLanguageName}/${solution.name}`
|
||||||
const testsPath = path.join(solution.challenge.path, 'test')
|
const testsPath = path.join(solution.challenge.path, 'test')
|
||||||
const testsFolders = await fs.promises.readdir(testsPath)
|
const testsFolders = await fs.promises.readdir(testsPath)
|
||||||
|
const testsNumbers = testsFolders.map((test) => Number(test)).sort((a, b) => a - b)
|
||||||
const tests: Test[] = []
|
const tests: Test[] = []
|
||||||
console.log(`${chalk.bold('Name:')} ${name}\n`)
|
console.log(`${chalk.bold('Name:')} ${name}\n`)
|
||||||
for (let index = 0; index < testsFolders.length; index++) {
|
for (const testNumber of testsNumbers) {
|
||||||
const currentTestIndex = index + 1
|
const loader = ora(`Test n°${testNumber}`).start()
|
||||||
const loader = ora(`Test n°${currentTestIndex}`).start()
|
|
||||||
try {
|
try {
|
||||||
const test = await Test.run({
|
const test = await Test.run({
|
||||||
path: path.join(testsPath, testsFolders[index]),
|
path: path.join(testsPath, testNumber.toString()),
|
||||||
index: currentTestIndex
|
index: testNumber
|
||||||
})
|
})
|
||||||
tests.push(test)
|
tests.push(test)
|
||||||
if (test.isSuccess) {
|
if (test.isSuccess) {
|
||||||
@ -138,7 +138,7 @@ export class Test implements TestOptions {
|
|||||||
await solution.test()
|
await solution.test()
|
||||||
console.log('\n------------------------------\n')
|
console.log('\n------------------------------\n')
|
||||||
}
|
}
|
||||||
console.log(Test.successMessage)
|
console.log(Test.SUCCESS_MESSAGE)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2194
package-lock.json
generated
2194
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@ -47,20 +47,21 @@
|
|||||||
"validate-npm-package-name": "3.0.0"
|
"validate-npm-package-name": "3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "14.1.0",
|
"@commitlint/cli": "15.0.0",
|
||||||
"@commitlint/config-conventional": "14.1.0",
|
"@commitlint/config-conventional": "15.0.0",
|
||||||
"@types/date-and-time": "0.13.0",
|
"@types/date-and-time": "0.13.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.3",
|
||||||
"@types/mock-fs": "4.13.1",
|
"@types/mock-fs": "4.13.1",
|
||||||
"@types/node": "16.11.7",
|
"@types/node": "16.11.11",
|
||||||
"@types/validate-npm-package-name": "3.0.3",
|
"@types/validate-npm-package-name": "3.0.3",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"jest": "27.3.1",
|
"get-stream": "6.0.1",
|
||||||
"markdownlint-cli": "0.29.0",
|
"jest": "27.4.2",
|
||||||
|
"markdownlint-cli": "0.30.0",
|
||||||
"mock-fs": "5.1.2",
|
"mock-fs": "5.1.2",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"ts-jest": "27.0.7",
|
"ts-jest": "27.0.7",
|
||||||
"ts-standard": "10.0.0",
|
"ts-standard": "11.0.0",
|
||||||
"typescript": "4.4.4"
|
"typescript": "4.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user