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:
parent
6427b3d273
commit
d6a6c706ce
@ -76,13 +76,16 @@ npm install --global
|
||||
programming-challenges --help
|
||||
|
||||
# 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
|
||||
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
|
||||
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
|
||||
|
@ -2,6 +2,7 @@ import { Builtins, Cli } from 'clipanion'
|
||||
|
||||
import { GenerateChallengeCommand } from './commands/generate/challenge.js'
|
||||
import { GenerateSolutionCommand } from './commands/generate/solution.js'
|
||||
import { RunSolutionCommand } from './commands/run/solution.js'
|
||||
import { RunTestCommand } from './commands/run/test.js'
|
||||
|
||||
export const cli = new Cli({
|
||||
@ -14,3 +15,4 @@ cli.register(Builtins.VersionCommand)
|
||||
cli.register(GenerateChallengeCommand)
|
||||
cli.register(GenerateSolutionCommand)
|
||||
cli.register(RunTestCommand)
|
||||
cli.register(RunSolutionCommand)
|
||||
|
148
cli/commands/run/__test__/solution.test.ts
Normal file
148
cli/commands/run/__test__/solution.test.ts
Normal 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
|
||||
)
|
||||
})
|
||||
})
|
72
cli/commands/run/solution.ts
Normal file
72
cli/commands/run/solution.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ export class RunTestCommand extends Command {
|
||||
})
|
||||
|
||||
public solutionName = Option.String('--solution', {
|
||||
description: 'solution',
|
||||
description: 'The solution name to run.',
|
||||
validator: typanion.isString()
|
||||
})
|
||||
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { performance } from 'node:perf_hooks'
|
||||
|
||||
import { execaCommand } from 'execa'
|
||||
import ora from 'ora'
|
||||
import ms from 'ms'
|
||||
|
||||
export interface DockerRunResult {
|
||||
stdout: string
|
||||
elapsedTimeMilliseconds: number
|
||||
}
|
||||
|
||||
export class Docker {
|
||||
static CONTAINER_TAG = 'programming-challenges'
|
||||
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(
|
||||
`docker run --interactive --rm ${Docker.CONTAINER_TAG}`,
|
||||
{
|
||||
@ -32,12 +39,17 @@ export class Docker {
|
||||
isValid = false
|
||||
}, Docker.MAXIMUM_TIMEOUT_MILLISECONDS)
|
||||
try {
|
||||
const start = performance.now()
|
||||
const { stdout, stderr } = await subprocess
|
||||
const end = performance.now()
|
||||
if (stderr.length !== 0) {
|
||||
throw new Error(stderr)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
return stdout
|
||||
return {
|
||||
stdout,
|
||||
elapsedTimeMilliseconds: end - start
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
|
@ -2,6 +2,9 @@ import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
import chalk from 'chalk'
|
||||
import ora from 'ora'
|
||||
|
||||
import {
|
||||
createTemporaryEmptyFolder,
|
||||
TEMPORARY_PATH
|
||||
@ -64,6 +67,24 @@ export class Solution implements SolutionOptions {
|
||||
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> {
|
||||
const { name, challengeName, programmingLanguageName, githubUser } = options
|
||||
const challenge = new Challenge({ name: challengeName })
|
||||
|
@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
|
||||
import ora from 'ora'
|
||||
import chalk from 'chalk'
|
||||
@ -84,17 +83,21 @@ export class Test implements TestOptions {
|
||||
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`)
|
||||
Test.printBenchmark(totalElapsedTimeMilliseconds)
|
||||
if (!isSuccess) {
|
||||
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> {
|
||||
const name = `${solution.challenge.name}/${solution.programmingLanguageName}/${solution.name}`
|
||||
const testsPath = path.join(solution.challenge.path, 'test')
|
||||
@ -154,9 +157,7 @@ export class Test implements TestOptions {
|
||||
|
||||
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 { stdout, elapsedTimeMilliseconds } = await docker.run(input)
|
||||
const test = new Test({
|
||||
path: options.path,
|
||||
index: options.index,
|
||||
@ -164,7 +165,7 @@ export class Test implements TestOptions {
|
||||
output,
|
||||
stdout,
|
||||
isSuccess: stdout === output,
|
||||
elapsedTimeMilliseconds: end - start
|
||||
elapsedTimeMilliseconds
|
||||
})
|
||||
return test
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user