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:
16
cli/cli.ts
Normal file
16
cli/cli.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Builtins, Cli } from 'clipanion'
|
||||
|
||||
import { GenerateChallengeCommand } from './commands/generate/challenge'
|
||||
import { GenerateSolutionCommand } from './commands/generate/solution'
|
||||
import { RunTestCommand } from './commands/run/test'
|
||||
|
||||
export const cli = new Cli({
|
||||
binaryLabel: 'programming-challenges',
|
||||
binaryName: 'programming-challenges',
|
||||
binaryVersion: '1.0.0'
|
||||
})
|
||||
cli.register(Builtins.HelpCommand)
|
||||
cli.register(Builtins.VersionCommand)
|
||||
cli.register(GenerateChallengeCommand)
|
||||
cli.register(GenerateSolutionCommand)
|
||||
cli.register(RunTestCommand)
|
44
cli/commands/generate/challenge.ts
Normal file
44
cli/commands/generate/challenge.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Command, Option } from 'clipanion'
|
||||
import * as typanion from 'typanion'
|
||||
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { Challenge } from '../../services/Challenge'
|
||||
|
||||
export class GenerateChallengeCommand extends Command {
|
||||
static paths = [['generate', 'challenge']]
|
||||
|
||||
static usage = {
|
||||
description: 'Create the basic files needed for a new challenge.'
|
||||
}
|
||||
|
||||
public challenge = Option.String('--challenge', {
|
||||
description: 'The new challenge name to generate.',
|
||||
required: true,
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public githubUser = Option.String('--github-user', {
|
||||
description: 'Your GitHub user.',
|
||||
required: true,
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
async execute (): Promise<number> {
|
||||
try {
|
||||
const challenge = await Challenge.generate({
|
||||
name: this.challenge,
|
||||
githubUser: this.githubUser
|
||||
})
|
||||
console.log(
|
||||
`${chalk.bold.green('Success:')} created the new challenge at ${
|
||||
challenge.path
|
||||
}.`
|
||||
)
|
||||
return 0
|
||||
} catch (error) {
|
||||
console.error(`${chalk.bold.red('Error:')} ${error.message as string}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
58
cli/commands/generate/solution.ts
Normal file
58
cli/commands/generate/solution.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Command, Option } from 'clipanion'
|
||||
import * as typanion from 'typanion'
|
||||
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { Solution } from '../../services/Solution'
|
||||
|
||||
export class GenerateSolutionCommand extends Command {
|
||||
static paths = [['generate', 'solution']]
|
||||
|
||||
static usage = {
|
||||
description: 'Create the basic files needed for a new solution.'
|
||||
}
|
||||
|
||||
public challenge = Option.String('--challenge', {
|
||||
description: 'The challenge name you want to generate a solution for.',
|
||||
required: true,
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public githubUser = Option.String('--github-user', {
|
||||
description: 'Your GitHub user.',
|
||||
required: true,
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public solutionName = Option.String('--solution', {
|
||||
description: 'The new solution name to generate.',
|
||||
required: true,
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public programmingLanguage = Option.String('--language', {
|
||||
description: 'The programming language to use to solve the challenge.',
|
||||
required: true,
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
async execute (): Promise<number> {
|
||||
try {
|
||||
const solution = await Solution.generate({
|
||||
name: this.solutionName,
|
||||
githubUser: this.githubUser,
|
||||
challengeName: this.challenge,
|
||||
programmingLanguageName: this.programmingLanguage
|
||||
})
|
||||
console.log(
|
||||
`${chalk.bold.green('Success:')} created the new solution at ${
|
||||
solution.path
|
||||
}.`
|
||||
)
|
||||
return 0
|
||||
} catch (error) {
|
||||
console.error(`${chalk.bold.red('Error:')} ${error.message as string}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
72
cli/commands/run/test.ts
Normal file
72
cli/commands/run/test.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Command, Option } from 'clipanion'
|
||||
import * as typanion from 'typanion'
|
||||
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { Solution } from '../../services/Solution'
|
||||
import { gitAffected } from '../../services/GitAffected'
|
||||
|
||||
const successMessage = `${chalk.bold.green('Success:')} Tests passed! 🎉`
|
||||
|
||||
export class RunTestCommand extends Command {
|
||||
static paths = [['run', 'test']]
|
||||
|
||||
static usage = {
|
||||
description:
|
||||
'Test if the solution is correct and display where it succeeds and fails.'
|
||||
}
|
||||
|
||||
public programmingLanguage = Option.String('--language', {
|
||||
description: 'The programming language to use to solve the challenge.',
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public challenge = Option.String('--challenge', {
|
||||
description: 'The challenge name where you want to test your solution.',
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public solutionName = Option.String('--solution', {
|
||||
description: 'solution',
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
public affected = Option.Boolean('--affected', false, {
|
||||
description: 'Only run the tests for the affected files in `git`.'
|
||||
})
|
||||
|
||||
async execute (): Promise<number> {
|
||||
console.log()
|
||||
try {
|
||||
if (this.affected) {
|
||||
const solutions = await gitAffected.getAffectedSolutions()
|
||||
for (const solution of solutions) {
|
||||
await solution.test()
|
||||
console.log('\n------------------------------\n')
|
||||
}
|
||||
console.log(successMessage)
|
||||
return 0
|
||||
}
|
||||
if (
|
||||
this.solutionName == null ||
|
||||
this.challenge == null ||
|
||||
this.programmingLanguage == null
|
||||
) {
|
||||
throw new Error(
|
||||
'You must specify all the options (`--challenge`, `--solution`, `--language`).'
|
||||
)
|
||||
}
|
||||
const solution = await Solution.get({
|
||||
name: this.solutionName,
|
||||
challengeName: this.challenge,
|
||||
programmingLanguageName: this.programmingLanguage
|
||||
})
|
||||
await solution.test()
|
||||
console.log(successMessage)
|
||||
return 0
|
||||
} catch (error) {
|
||||
console.error(`\n${chalk.bold.red('Error:')} ${error.message as string}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
11
cli/index.ts
Normal file
11
cli/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import { Cli } from 'clipanion'
|
||||
|
||||
import { cli } from './cli'
|
||||
|
||||
const [, , ...args] = process.argv
|
||||
|
||||
cli.runExit(args, Cli.defaultContext).catch(() => {
|
||||
console.error('Error occurred...')
|
||||
process.exit(1)
|
||||
})
|
45
cli/services/Challenge.ts
Normal file
45
cli/services/Challenge.ts
Normal 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
33
cli/services/Docker.ts
Normal 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()
|
33
cli/services/GitAffected.ts
Normal file
33
cli/services/GitAffected.ts
Normal 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
104
cli/services/Solution.ts
Normal 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
100
cli/services/Template.ts
Normal 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
151
cli/services/Test.ts
Normal 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
|
||||
}
|
||||
}
|
38
cli/utils/__test__/copyDirectory.test.ts
Normal file
38
cli/utils/__test__/copyDirectory.test.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import fsMock from 'mock-fs'
|
||||
import fs from 'fs'
|
||||
|
||||
import { copyDirectory } from '../copyDirectory'
|
||||
|
||||
describe('utils/copyDirectory', () => {
|
||||
afterEach(async () => {
|
||||
fsMock.restore()
|
||||
})
|
||||
|
||||
it('copy the files', async () => {
|
||||
fsMock({
|
||||
'/source': {
|
||||
'default.png': '',
|
||||
'index.ts': '',
|
||||
'.npmignore': ''
|
||||
},
|
||||
'/destination': {}
|
||||
})
|
||||
|
||||
let destinationDirectoryContent = await fs.promises.readdir('/destination')
|
||||
let sourceDirectoryContent = await fs.promises.readdir('/source')
|
||||
expect(destinationDirectoryContent.length).toEqual(0)
|
||||
expect(sourceDirectoryContent.length).toEqual(3)
|
||||
|
||||
await copyDirectory('/source', '/destination')
|
||||
destinationDirectoryContent = await fs.promises.readdir('/destination')
|
||||
sourceDirectoryContent = await fs.promises.readdir('/source')
|
||||
expect(destinationDirectoryContent.length).toEqual(3)
|
||||
expect(sourceDirectoryContent.length).toEqual(3)
|
||||
expect(destinationDirectoryContent).toEqual(
|
||||
expect.arrayContaining(['default.png', 'index.ts', '.npmignore'])
|
||||
)
|
||||
expect(sourceDirectoryContent).toEqual(
|
||||
expect.arrayContaining(['default.png', 'index.ts', '.npmignore'])
|
||||
)
|
||||
})
|
||||
})
|
33
cli/utils/__test__/createTemporaryEmptyFolder.test.ts
Normal file
33
cli/utils/__test__/createTemporaryEmptyFolder.test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import fsMock from 'mock-fs'
|
||||
import fs from 'fs'
|
||||
|
||||
import {
|
||||
TEMPORARY_PATH,
|
||||
createTemporaryEmptyFolder
|
||||
} from '../createTemporaryEmptyFolder'
|
||||
import { isExistingPath } from '../isExistingPath'
|
||||
|
||||
describe('utils/createTemporaryEmptyFolder', () => {
|
||||
afterEach(async () => {
|
||||
fsMock.restore()
|
||||
})
|
||||
|
||||
it('should create the temporary folder', async () => {
|
||||
fsMock({})
|
||||
expect(await isExistingPath(TEMPORARY_PATH)).toBeFalsy()
|
||||
await createTemporaryEmptyFolder()
|
||||
expect(await isExistingPath(TEMPORARY_PATH)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should remove and create again the temporary folder', async () => {
|
||||
fsMock({
|
||||
[TEMPORARY_PATH]: {
|
||||
'file.txt': ''
|
||||
}
|
||||
})
|
||||
expect(await isExistingPath(TEMPORARY_PATH)).toBeTruthy()
|
||||
expect((await fs.promises.readdir(TEMPORARY_PATH)).length).toEqual(1)
|
||||
await createTemporaryEmptyFolder()
|
||||
expect((await fs.promises.readdir(TEMPORARY_PATH)).length).toEqual(0)
|
||||
})
|
||||
})
|
23
cli/utils/__test__/isExistingPath.test.ts
Normal file
23
cli/utils/__test__/isExistingPath.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import fsMock from 'mock-fs'
|
||||
|
||||
import { isExistingPath } from '../isExistingPath'
|
||||
|
||||
describe('utils/isExistingFile', () => {
|
||||
afterEach(async () => {
|
||||
fsMock.restore()
|
||||
})
|
||||
|
||||
it('should return true if the file exists', async () => {
|
||||
fsMock({
|
||||
'/file.txt': ''
|
||||
})
|
||||
expect(await isExistingPath('/file.txt')).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should return false if the file doesn't exists", async () => {
|
||||
fsMock({
|
||||
'/file.txt': ''
|
||||
})
|
||||
expect(await isExistingPath('/randomfile.txt')).toBeFalsy()
|
||||
})
|
||||
})
|
20
cli/utils/copyDirectory.ts
Normal file
20
cli/utils/copyDirectory.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export async function copyDirectory (
|
||||
source: string,
|
||||
destination: string
|
||||
): Promise<void> {
|
||||
const filesToCreate = await fs.promises.readdir(source)
|
||||
for (const file of filesToCreate) {
|
||||
const originalFilePath = path.join(source, file)
|
||||
const stats = await fs.promises.stat(originalFilePath)
|
||||
if (stats.isFile()) {
|
||||
const writePath = path.join(destination, file)
|
||||
await fs.promises.copyFile(originalFilePath, writePath)
|
||||
} else if (stats.isDirectory()) {
|
||||
await fs.promises.mkdir(path.join(destination, file), { recursive: true })
|
||||
await copyDirectory(path.join(source, file), path.join(destination, file))
|
||||
}
|
||||
}
|
||||
}
|
13
cli/utils/createTemporaryEmptyFolder.ts
Normal file
13
cli/utils/createTemporaryEmptyFolder.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
import { isExistingPath } from '../utils/isExistingPath'
|
||||
|
||||
export const TEMPORARY_PATH = path.join(__dirname, '..', '..', 'temp')
|
||||
|
||||
export const createTemporaryEmptyFolder = async (): Promise<void> => {
|
||||
if (await isExistingPath(TEMPORARY_PATH)) {
|
||||
await fs.promises.rm(TEMPORARY_PATH, { recursive: true, force: true })
|
||||
}
|
||||
await fs.promises.mkdir(TEMPORARY_PATH)
|
||||
}
|
10
cli/utils/isExistingPath.ts
Normal file
10
cli/utils/isExistingPath.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import fs from 'fs'
|
||||
|
||||
export const isExistingPath = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.promises.access(path, fs.constants.F_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user