1
1
mirror of https://github.com/theoludwig/programming-challenges.git synced 2025-05-18 12:02:53 +02:00

feat: rewrite programming-challenges CLI (#3)

This commit is contained in:
Divlo
2021-06-09 20:31:45 +02:00
committed by GitHub
parent 7aa12f313e
commit 677a55a9d8
256 changed files with 16829 additions and 1881 deletions

45
cli/services/Challenge.ts Normal file
View File

@ -0,0 +1,45 @@
import path from 'path'
import fs from 'fs'
import validateProjectName from 'validate-npm-package-name'
import { isExistingPath } from '../utils/isExistingPath'
import { template } from './Template'
export interface ChallengeOptions {
name: string
}
export interface GenerateChallengeOptions extends ChallengeOptions {
githubUser: string
}
export class Challenge implements ChallengeOptions {
public name: string
public path: string
constructor (options: ChallengeOptions) {
const { name } = options
this.name = name
this.path = path.join(__dirname, '..', '..', 'challenges', name)
}
static async generate (options: GenerateChallengeOptions): Promise<Challenge> {
const { name, githubUser } = options
const challenge = new Challenge({ name })
if (await isExistingPath(challenge.path)) {
throw new Error(`The challenge already exists: ${name}.`)
}
const isValidName = validateProjectName(name).validForNewPackages
if (!isValidName) {
throw new Error('Invalid challenge name.')
}
await fs.promises.mkdir(challenge.path)
await template.challenge({
destination: challenge.path,
githubUser,
name
})
return challenge
}
}

33
cli/services/Docker.ts Normal file
View File

@ -0,0 +1,33 @@
import execa from 'execa'
import ora from 'ora'
const CONTAINER_TAG = 'programming-challenges'
class Docker {
public async build (): Promise<void> {
const loader = ora('Building the Docker image').start()
try {
await execa.command(`docker build --tag=${CONTAINER_TAG} ./`)
loader.stop()
} catch (error) {
loader.fail()
throw error
}
}
public async run (input: string): Promise<string> {
const subprocess = execa.command(
`docker run --interactive --rm ${CONTAINER_TAG}`,
{
input
}
)
const { stdout, stderr } = await subprocess
if (stderr.length !== 0) {
throw new Error(stderr)
}
return stdout
}
}
export const docker = new Docker()

View File

@ -0,0 +1,33 @@
import simpleGit from 'simple-git'
import { Challenge } from './Challenge'
import { Solution } from './Solution'
const git = simpleGit()
const solutionsRegex = new RegExp(
/challenges\/[\s\S]*\/solutions\/(c|cpp|dart|javascript|python|rust|typescript)\/[\s\S]*\/solution.(c|cpp|dart|js|py|rs|ts)/
)
class GitAffected {
public async getAffectedSolutions (): Promise<Solution[]> {
await git.add('.')
const diff = await git.diff(['--name-only', '--staged'])
const affectedSolutionsPaths = diff.split('\n').filter((currentDiff) => {
return solutionsRegex.test(currentDiff)
})
return affectedSolutionsPaths.map((solution) => {
const [, challengeName, , programmingLanguageName, solutionName] =
solution.split('/')
return new Solution({
challenge: new Challenge({
name: challengeName
}),
name: solutionName,
programmingLanguageName
})
})
}
}
export const gitAffected = new GitAffected()

104
cli/services/Solution.ts Normal file
View File

@ -0,0 +1,104 @@
import path from 'path'
import {
createTemporaryEmptyFolder,
TEMPORARY_PATH
} from '../utils/createTemporaryEmptyFolder'
import { isExistingPath } from '../utils/isExistingPath'
import { Challenge } from './Challenge'
import { copyDirectory } from '../utils/copyDirectory'
import { template } from './Template'
import { docker } from './Docker'
import { Test } from './Test'
export interface GetSolutionOptions {
programmingLanguageName: string
challengeName: string
name: string
}
export interface GenerateSolutionOptions extends GetSolutionOptions {
githubUser: string
}
export interface SolutionOptions {
programmingLanguageName: string
challenge: Challenge
name: string
}
export class Solution implements SolutionOptions {
public programmingLanguageName: string
public challenge: Challenge
public name: string
public path: string
constructor (options: SolutionOptions) {
const { programmingLanguageName, challenge, name } = options
this.programmingLanguageName = programmingLanguageName
this.challenge = challenge
this.name = name
this.path = path.join(
challenge.path,
'solutions',
programmingLanguageName,
name
)
}
private async prepareTemporaryFolder (): Promise<void> {
await createTemporaryEmptyFolder()
await copyDirectory(this.path, TEMPORARY_PATH)
await template.docker({
programmingLanguage: this.programmingLanguageName,
destination: TEMPORARY_PATH
})
process.chdir(TEMPORARY_PATH)
}
public async test (): Promise<void> {
await this.prepareTemporaryFolder()
await docker.build()
await Test.runAll(this)
}
static async generate (options: GenerateSolutionOptions): Promise<Solution> {
const { name, challengeName, programmingLanguageName, githubUser } = options
const challenge = new Challenge({ name: challengeName })
if (!(await isExistingPath(challenge.path))) {
throw new Error(`The challenge doesn't exist yet: ${name}.`)
}
const solution = new Solution({
name,
challenge,
programmingLanguageName
})
if (await isExistingPath(solution.path)) {
throw new Error('The solution already exists.')
}
await template.solution({
challengeName: challenge.name,
destination: solution.path,
githubUser,
programmingLanguageName: solution.programmingLanguageName,
name: solution.name
})
return solution
}
static async get (options: GetSolutionOptions): Promise<Solution> {
const { name, challengeName, programmingLanguageName } = options
const challenge = new Challenge({
name: challengeName
})
const solution = new Solution({
name,
challenge,
programmingLanguageName
})
if (!(await isExistingPath(solution.path))) {
throw new Error('The solution was not found.')
}
return solution
}
}

100
cli/services/Template.ts Normal file
View File

@ -0,0 +1,100 @@
import path from 'path'
import fs from 'fs'
import { replaceInFile } from 'replace-in-file'
import date from 'date-and-time'
import { copyDirectory } from '../utils/copyDirectory'
import { isExistingPath } from '../utils/isExistingPath'
const TEMPLATE_PATH = path.join(__dirname, '..', '..', 'templates')
const TEMPLATE_DOCKER_PATH = path.join(TEMPLATE_PATH, 'docker')
const TEMPLATE_CHALLENGE_PATH = path.join(TEMPLATE_PATH, 'challenge')
const TEMPLATE_SOLUTION_PATH = path.join(TEMPLATE_PATH, 'solution')
const TEMPLATE_SOLUTION_BASE_PATH = path.join(TEMPLATE_SOLUTION_PATH, 'base')
export interface TemplateDockerOptions {
programmingLanguage: string
destination: string
}
export interface TemplateChallengeOptions {
name: string
githubUser?: string
destination: string
}
export interface TemplateSolutionOptions {
challengeName: string
programmingLanguageName: string
name: string
githubUser?: string
destination: string
}
export interface ReplaceInDestinationOptions {
destination: string
name: string
description: string
}
class Template {
private getDescription (githubUser?: string): string {
const dateString = date.format(new Date(), 'D MMMM Y', true)
let description = 'Created'
if (githubUser != null) {
description += ` by [@${githubUser}](https://github.com/${githubUser})`
}
description += ` on ${dateString}.`
return description
}
private async replaceInDestination (options: ReplaceInDestinationOptions): Promise<void> {
const { name, description, destination } = options
const readmePath = path.join(destination, 'README.md')
await replaceInFile({
files: [readmePath],
from: /{{ name }}/g,
to: name
})
await replaceInFile({
files: [readmePath],
from: /{{ description }}/g,
to: description
})
}
public async docker (options: TemplateDockerOptions): Promise<void> {
const { programmingLanguage, destination } = options
const sourcePath = path.join(TEMPLATE_DOCKER_PATH, programmingLanguage)
await copyDirectory(sourcePath, destination)
}
public async solution (options: TemplateSolutionOptions): Promise<void> {
const { destination, githubUser, name, challengeName, programmingLanguageName } = options
const templateLanguagePath = path.join(TEMPLATE_SOLUTION_PATH, programmingLanguageName)
if (!(await isExistingPath(templateLanguagePath))) {
throw new Error('This programming language is not supported yet.')
}
await fs.promises.mkdir(destination, { recursive: true })
await copyDirectory(templateLanguagePath, destination)
await copyDirectory(TEMPLATE_SOLUTION_BASE_PATH, destination)
await this.replaceInDestination({
name: `${challengeName}/${programmingLanguageName}/${name}`,
description: this.getDescription(githubUser),
destination
})
}
public async challenge (options: TemplateChallengeOptions): Promise<void> {
const { destination, githubUser, name } = options
await copyDirectory(TEMPLATE_CHALLENGE_PATH, destination)
await this.replaceInDestination({
name: name,
description: this.getDescription(githubUser),
destination
})
}
}
export const template = new Template()

151
cli/services/Test.ts Normal file
View File

@ -0,0 +1,151 @@
import fs from 'fs'
import path from 'path'
import { performance } from 'perf_hooks'
import ora from 'ora'
import chalk from 'chalk'
import { table } from 'table'
import { Solution } from './Solution'
import { docker } from './Docker'
export interface InputOutput {
input: string
output: string
}
export interface TestRunOptions {
index: number
path: string
}
export interface TestOptions {
index: number
path: string
isSuccess: boolean
input: string
output: string
stdout: string
elapsedTimeMilliseconds: number
}
export class Test implements TestOptions {
public index: number
public path: string
public isSuccess: boolean
public input: string
public output: string
public stdout: string
public elapsedTimeMilliseconds: number
constructor (options: TestOptions) {
this.index = options.index
this.path = options.path
this.isSuccess = options.isSuccess
this.input = options.input
this.output = options.output
this.stdout = options.stdout
this.elapsedTimeMilliseconds = options.elapsedTimeMilliseconds
}
static async printResult (tests: Test[]): Promise<void> {
const tableResult = [
[
chalk.bold('N°'),
chalk.bold('Input'),
chalk.bold('Expected'),
chalk.bold('Received')
]
]
let totalFailedTests = 0
let totalCorrectTests = 0
let totalElapsedTimeMilliseconds = 0
for (const test of tests) {
if (!test.isSuccess) {
const expected = test.output.split('\n').join('"\n"')
const received = test.stdout.split('\n').join('"\n"')
tableResult.push([
test.index.toString(),
`"${test.input}"`,
`"${expected}"`,
`"${received}"`
])
totalFailedTests += 1
} else {
totalCorrectTests += 1
}
totalElapsedTimeMilliseconds += test.elapsedTimeMilliseconds
}
const isSuccess = totalCorrectTests === tests.length
if (isSuccess) {
console.log()
} else {
console.log()
console.log(table(tableResult))
}
const elapsedTime = totalElapsedTimeMilliseconds / 1000
const testsResult = isSuccess
? chalk.bold.green(`${totalCorrectTests} passed`)
: chalk.bold.red(`${totalFailedTests} failed`)
console.log(`${chalk.bold('Tests:')} ${testsResult}, ${tests.length} total`)
console.log(`${chalk.bold('Benchmark:')} ${elapsedTime} seconds`)
if (!isSuccess) {
throw new Error('Tests failed, try again!')
}
}
static async runAll (solution: Solution): Promise<void> {
const name = `${solution.challenge.name}/${solution.programmingLanguageName}/${solution.name}`
const testsPath = path.join(solution.challenge.path, 'test')
const testsFolders = await fs.promises.readdir(testsPath)
const tests: Test[] = []
console.log(`${chalk.bold('Name:')} ${name}\n`)
for (let index = 0; index < testsFolders.length; index++) {
const currentTestIndex = index + 1
const loader = ora(`Test n°${currentTestIndex}`).start()
try {
const test = await Test.run({
path: path.join(testsPath, testsFolders[index]),
index: currentTestIndex
})
tests.push(test)
if (test.isSuccess) {
loader.succeed()
} else {
loader.fail()
}
} catch (error) {
loader.fail()
throw error
}
}
await Test.printResult(tests)
}
static async getInputOutput (testPath: string): Promise<InputOutput> {
const inputPath = path.join(testPath, 'input.txt')
const outputPath = path.join(testPath, 'output.txt')
const input = await fs.promises.readFile(inputPath, { encoding: 'utf-8' })
const output = await fs.promises.readFile(outputPath, {
encoding: 'utf-8'
})
return { input, output }
}
static async run (options: TestRunOptions): Promise<Test> {
const { input, output } = await Test.getInputOutput(options.path)
const start = performance.now()
const stdout = await docker.run(input)
const end = performance.now()
const test = new Test({
path: options.path,
index: options.index,
input,
output,
stdout,
isSuccess: stdout === output,
elapsedTimeMilliseconds: end - start
})
return test
}
}