1
1
mirror of https://github.com/theoludwig/programming-challenges.git synced 2024-12-08 00:45:29 +01:00

feat(cli): add commands/run/solution

This commit is contained in:
Divlo 2022-08-30 15:48:07 +02:00
parent 6427b3d273
commit d6a6c706ce
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
8 changed files with 272 additions and 13 deletions

View File

@ -76,13 +76,16 @@ npm install --global
programming-challenges --help programming-challenges --help
# Generate a new challenge # Generate a new challenge
programming-challenges generate challenge --github-user="YourGitHubName" --challenge="challenge-name" programming-challenges generate challenge --github-user="YourGitHubName" --challenge="hello-world"
# Generate a new solution # Generate a new solution
programming-challenges generate solution --github-user="YourGitHubName" --challenge="challenge-name" --solution="function" --language="python" programming-challenges generate solution --github-user="YourGitHubName" --challenge="hello-world" --solution="function" --language="python"
# Test a solution # Test a solution
programming-challenges run test --challenge="challenge-name" --solution="function" --language="python" programming-challenges run test --challenge="hello-world" --solution="function" --language="python"
# Run a solution with specific `input.txt` file
programming-challenges run solution --challenge="hello-world" --solution="function" --language="python" --input-path="./challenges/hello-world/test/1/input.txt" --output
``` ```
## 💡 Contributing ## 💡 Contributing

View File

@ -2,6 +2,7 @@ import { Builtins, Cli } from 'clipanion'
import { GenerateChallengeCommand } from './commands/generate/challenge.js' import { GenerateChallengeCommand } from './commands/generate/challenge.js'
import { GenerateSolutionCommand } from './commands/generate/solution.js' import { GenerateSolutionCommand } from './commands/generate/solution.js'
import { RunSolutionCommand } from './commands/run/solution.js'
import { RunTestCommand } from './commands/run/test.js' import { RunTestCommand } from './commands/run/test.js'
export const cli = new Cli({ export const cli = new Cli({
@ -14,3 +15,4 @@ cli.register(Builtins.VersionCommand)
cli.register(GenerateChallengeCommand) cli.register(GenerateChallengeCommand)
cli.register(GenerateSolutionCommand) cli.register(GenerateSolutionCommand)
cli.register(RunTestCommand) cli.register(RunTestCommand)
cli.register(RunSolutionCommand)

View File

@ -0,0 +1,148 @@
import { PassThrough } from 'node:stream'
import path from 'node:path'
import tap from 'tap'
import sinon from 'sinon'
import chalk from 'chalk'
import { cli } from '../../../cli.js'
const input = ['run', 'solution']
const challenge = 'hello-world'
const language = 'c'
const solution = 'function'
const inputPath = path.join(
process.cwd(),
'challenges',
challenge,
'test',
'1',
'input.txt'
)
const inputChallenge = `--challenge=${challenge}`
const inputLanguage = `--language=${language}`
const inputSolution = `--solution=${solution}`
const inputInputPath = `--input-path=${inputPath}`
await tap.test('programming-challenges run solution', async (t) => {
t.afterEach(() => {
sinon.restore()
})
await t.test('succeeds', async (t) => {
sinon.stub(console, 'log').value(() => {})
const consoleLogSpy = sinon.spy(console, 'log')
const stream = new PassThrough()
const exitCode = await cli.run(
[
...input,
inputChallenge,
inputSolution,
inputLanguage,
inputInputPath,
'--output'
],
{
stdin: process.stdin,
stdout: stream,
stderr: stream
}
)
stream.end()
t.equal(exitCode, 0)
t.equal(consoleLogSpy.calledWith(`Hello, world!`), true)
})
await t.test("fails with solution that doesn't exist", async (t) => {
sinon.stub(console, 'error').value(() => {})
const consoleErrorSpy = sinon.spy(console, 'error')
const stream = new PassThrough()
const invalidSolution = 'invalid'
const inputInvalidSolution = `--solution=${invalidSolution}`
const exitCode = await cli.run(
[
...input,
inputChallenge,
inputInvalidSolution,
inputLanguage,
inputInputPath
],
{
stdin: process.stdin,
stdout: stream,
stderr: stream
}
)
stream.end()
t.equal(exitCode, 1)
t.equal(
consoleErrorSpy.calledWith(
chalk.bold.red('Error:') + ' The solution was not found.'
),
true
)
})
await t.test('fails with invalid language', async (t) => {
sinon.stub(console, 'error').value(() => {})
const consoleErrorSpy = sinon.spy(console, 'error')
const stream = new PassThrough()
const invalidLanguage = 'invalid'
const inputInvalidLanguage = `--language=${invalidLanguage}`
const exitCode = await cli.run(
[
...input,
inputChallenge,
inputSolution,
inputInvalidLanguage,
inputInputPath
],
{
stdin: process.stdin,
stdout: stream,
stderr: stream
}
)
stream.end()
t.equal(exitCode, 1)
t.equal(
consoleErrorSpy.calledWith(
chalk.bold.red('Error:') +
' This programming language is not supported yet.'
),
true
)
})
await t.test('fails with invalid `input-path`', async (t) => {
sinon.stub(console, 'error').value(() => {})
const consoleErrorSpy = sinon.spy(console, 'error')
const stream = new PassThrough()
const invalidInputPath = 'invalid'
const inputInvalidInputPath = `--input-path=${invalidInputPath}`
const inputPath = path.resolve(process.cwd(), invalidInputPath)
const exitCode = await cli.run(
[
...input,
inputChallenge,
inputSolution,
inputLanguage,
inputInvalidInputPath
],
{
stdin: process.stdin,
stdout: stream,
stderr: stream
}
)
stream.end()
t.equal(exitCode, 1)
t.equal(
consoleErrorSpy.calledWith(
chalk.bold.red('Error:') +
` The \`input-path\` doesn't exist: ${inputPath}.`
),
true
)
})
})

View File

@ -0,0 +1,72 @@
import path from 'node:path'
import fs from 'node:fs'
import { Command, Option } from 'clipanion'
import * as typanion from 'typanion'
import chalk from 'chalk'
import { isExistingPath } from '../../utils/isExistingPath.js'
import { template } from '../../services/Template.js'
import { Solution } from '../../services/Solution.js'
export class RunSolutionCommand extends Command {
static paths = [['run', 'solution']]
static usage = {
description: 'Run the solution with the given `input.txt` file.'
}
public programmingLanguage = Option.String('--language', {
description: 'The programming language used to solve the challenge.',
required: true,
validator: typanion.isString()
})
public challenge = Option.String('--challenge', {
description: 'The challenge name where you want to run your solution.',
required: true,
validator: typanion.isString()
})
public solutionName = Option.String('--solution', {
description: 'The solution name to run.',
required: true,
validator: typanion.isString()
})
public inputPathUser = Option.String('--input-path', {
description: 'The input file path to use.',
required: true,
validator: typanion.isString()
})
public output = Option.Boolean('--output', false, {
description: 'Display the output of the solution.'
})
async execute(): Promise<number> {
console.log()
try {
await template.verifySupportedProgrammingLanguage(
this.programmingLanguage
)
const solution = await Solution.get({
name: this.solutionName,
challengeName: this.challenge,
programmingLanguageName: this.programmingLanguage
})
const inputPath = path.resolve(process.cwd(), this.inputPathUser)
if (!(await isExistingPath(inputPath))) {
throw new Error(`The \`input-path\` doesn't exist: ${inputPath}.`)
}
const input = await fs.promises.readFile(inputPath, { encoding: 'utf-8' })
await solution.run(input, this.output)
return 0
} catch (error) {
if (error instanceof Error) {
console.error(`${chalk.bold.red('Error:')} ${error.message}`)
}
return 1
}
}
}

View File

@ -26,7 +26,7 @@ export class RunTestCommand extends Command {
}) })
public solutionName = Option.String('--solution', { public solutionName = Option.String('--solution', {
description: 'solution', description: 'The solution name to run.',
validator: typanion.isString() validator: typanion.isString()
}) })

View File

@ -1,7 +1,14 @@
import { performance } from 'node:perf_hooks'
import { execaCommand } from 'execa' import { execaCommand } from 'execa'
import ora from 'ora' import ora from 'ora'
import ms from 'ms' import ms from 'ms'
export interface DockerRunResult {
stdout: string
elapsedTimeMilliseconds: number
}
export class Docker { export class Docker {
static CONTAINER_TAG = 'programming-challenges' static CONTAINER_TAG = 'programming-challenges'
static SIGSEGV_EXIT_CODE = 139 static SIGSEGV_EXIT_CODE = 139
@ -19,7 +26,7 @@ export class Docker {
} }
} }
public async run(input: string): Promise<string> { public async run(input: string): Promise<DockerRunResult> {
const subprocess = execaCommand( const subprocess = execaCommand(
`docker run --interactive --rm ${Docker.CONTAINER_TAG}`, `docker run --interactive --rm ${Docker.CONTAINER_TAG}`,
{ {
@ -32,12 +39,17 @@ export class Docker {
isValid = false isValid = false
}, Docker.MAXIMUM_TIMEOUT_MILLISECONDS) }, Docker.MAXIMUM_TIMEOUT_MILLISECONDS)
try { try {
const start = performance.now()
const { stdout, stderr } = await subprocess const { stdout, stderr } = await subprocess
const end = performance.now()
if (stderr.length !== 0) { if (stderr.length !== 0) {
throw new Error(stderr) throw new Error(stderr)
} }
clearTimeout(timeout) clearTimeout(timeout)
return stdout return {
stdout,
elapsedTimeMilliseconds: end - start
}
} catch (error: any) { } catch (error: any) {
if (!isValid) { if (!isValid) {
throw new Error( throw new Error(

View File

@ -2,6 +2,9 @@ import { fileURLToPath } from 'node:url'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import chalk from 'chalk'
import ora from 'ora'
import { import {
createTemporaryEmptyFolder, createTemporaryEmptyFolder,
TEMPORARY_PATH TEMPORARY_PATH
@ -64,6 +67,24 @@ export class Solution implements SolutionOptions {
await Test.runAll(this) await Test.runAll(this)
} }
public async run(input: string, output: boolean = false): Promise<void> {
await this.prepareTemporaryFolder()
await docker.build()
const loader = ora('Running...').start()
try {
const { stdout, elapsedTimeMilliseconds } = await docker.run(input)
loader.succeed(chalk.bold.green('Success!'))
Test.printBenchmark(elapsedTimeMilliseconds)
if (output) {
console.log(`${chalk.bold('Output:')}`)
console.log(stdout)
}
} catch (error) {
loader.fail()
throw error
}
}
static async generate(options: GenerateSolutionOptions): Promise<Solution> { static async generate(options: GenerateSolutionOptions): Promise<Solution> {
const { name, challengeName, programmingLanguageName, githubUser } = options const { name, challengeName, programmingLanguageName, githubUser } = options
const challenge = new Challenge({ name: challengeName }) const challenge = new Challenge({ name: challengeName })

View File

@ -1,6 +1,5 @@
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { performance } from 'node:perf_hooks'
import ora from 'ora' import ora from 'ora'
import chalk from 'chalk' import chalk from 'chalk'
@ -84,17 +83,21 @@ export class Test implements TestOptions {
console.log() console.log()
console.log(table(tableResult)) console.log(table(tableResult))
} }
const elapsedTime = totalElapsedTimeMilliseconds / 1000
const testsResult = isSuccess const testsResult = isSuccess
? chalk.bold.green(`${totalCorrectTests} passed`) ? chalk.bold.green(`${totalCorrectTests} passed`)
: chalk.bold.red(`${totalFailedTests} failed`) : chalk.bold.red(`${totalFailedTests} failed`)
console.log(`${chalk.bold('Tests:')} ${testsResult}, ${tests.length} total`) console.log(`${chalk.bold('Tests:')} ${testsResult}, ${tests.length} total`)
console.log(`${chalk.bold('Benchmark:')} ${elapsedTime} seconds`) Test.printBenchmark(totalElapsedTimeMilliseconds)
if (!isSuccess) { if (!isSuccess) {
throw new Error('Tests failed, try again!') throw new Error('Tests failed, try again!')
} }
} }
static printBenchmark(elapsedTimeMilliseconds: number): void {
const elapsedTime = elapsedTimeMilliseconds / 1000
console.log(`${chalk.bold('Benchmark:')} ${elapsedTime} seconds`)
}
static async runAll(solution: Solution): Promise<void> { static async runAll(solution: Solution): Promise<void> {
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')
@ -154,9 +157,7 @@ export class Test implements TestOptions {
static async run(options: TestRunOptions): Promise<Test> { static async run(options: TestRunOptions): Promise<Test> {
const { input, output } = await Test.getInputOutput(options.path) const { input, output } = await Test.getInputOutput(options.path)
const start = performance.now() const { stdout, elapsedTimeMilliseconds } = await docker.run(input)
const stdout = await docker.run(input)
const end = performance.now()
const test = new Test({ const test = new Test({
path: options.path, path: options.path,
index: options.index, index: options.index,
@ -164,7 +165,7 @@ export class Test implements TestOptions {
output, output,
stdout, stdout,
isSuccess: stdout === output, isSuccess: stdout === output,
elapsedTimeMilliseconds: end - start elapsedTimeMilliseconds
}) })
return test return test
} }