Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add --probSpecsDir option #65

Merged
merged 4 commits into from
Oct 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: tests

on: [push, pull_request]

jobs:
all_tests:
strategy:
fail-fast: false
matrix:
target:
- os: linux
arch: 64bit
- os: mac
arch: 64bit
- os: windows
arch: 64bit
include:
- target:
os: linux
builder: ubuntu-18.04
- target:
os: mac
builder: macos-10.15
- target:
os: windows
builder: windows-2019

name: "${{ matrix.target.os }}-${{ matrix.target.arch }}"
runs-on: ${{ matrix.builder }}
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Install Nim
uses: jiro4989/setup-nim-action@v1
with:
nim-version: "1.4.0"

- name: Install our Nimble dependencies
run: nimble -y install --depsOnly

- name: Run `tests/all_tests.nim`
run: nim c --styleCheck:error -r ./tests/all_tests.nim
11 changes: 9 additions & 2 deletions src/cli.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ type
exercise*: Option[string]
mode*: Mode
verbosity*: Verbosity
probSpecsDir*: Option[string]

Opt = enum
optExercise, optCheck, optMode, optVerbosity, optHelp, optVersion
optExercise, optCheck, optMode, optVerbosity, optProbSpecsDir,
optHelp, optVersion

OptKey = tuple
short: string
Expand All @@ -32,6 +34,7 @@ const
("c", "check"),
("m", "mode"),
("o", "verbosity"),
("p", "probSpecsDir"),
("h", "help"),
("v", "version"),
]
Expand All @@ -54,6 +57,7 @@ Options:
-{optCheck.short}, --{optCheck.long} Check if there are missing tests. Doesn't update the tests. Terminates with a non-zero exit code if one or more tests are missing
-{optMode.short}, --{optMode.long} <mode> What to do with missing test cases. Allowed values: c[hoose], i[nclude], e[xclude]
-{optVerbosity.short}, --{optVerbosity.long} <verbosity> The verbosity of output. Allowed values: q[uiet], n[ormal], d[etailed]
-{optProbSpecsDir.short}, --{optProbSpecsDir.long} <dir> Use this `problem-specifications` directory, rather than cloning temporarily
-{optHelp.short}, --{optHelp.long} Show this help message and exit
-{optVersion.short}, --{optVersion.long} Show this tool's version information and exit"""

Expand All @@ -63,7 +67,7 @@ proc showVersion =
echo &"Canonical Data Syncer v{NimblePkgVersion}"
quit(0)

proc showError(s: string) =
proc showError*(s: string) =
stdout.styledWrite(fgRed, "Error: ")
stdout.write(s)
stdout.write("\n\n")
Expand Down Expand Up @@ -134,6 +138,9 @@ proc processCmdLine*: Conf =
of optVerbosity.short, optVerbosity.long:
showErrorForMissingVal(kind, key, val)
result.verbosity = parseVerbosity(kind, key, val)
of optProbSpecsDir.short, optProbSpecsDir.long:
showErrorForMissingVal(kind, key, val)
result.probSpecsDir = some(val)
of optHelp.short, optHelp.long:
showHelp()
of optVersion.short, optVersion.long:
Expand Down
115 changes: 95 additions & 20 deletions src/probspecs.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[json, options, os, osproc, sequtils, strformat, strutils]
import std/[json, options, os, osproc, sequtils, strformat, strscans, strutils]
import cli, logger

type
Expand Down Expand Up @@ -30,16 +30,6 @@ proc clone(repo: ProbSpecsRepo) =
logNormal(&"Cloning the problem-specifications repo into {repo.dir}...")
execCmdException(cmd, "Could not clone problem-specifications repo")

proc grainsWorkaround(repo: ProbSpecsRepo) =
## Overwrites the canonical data file for `grains` so that it no longer
## contains integers that are too large to store in a 64-bit signed integer.
## Otherwise, we get an error when parsing it as JSON.
let grainsPath = repo.dir / "exercises" / "grains" / "canonical-data.json"
let s = readFile(grainsPath).multiReplace(
("92233720368547758", "92233720368547758.0"),
("184467440737095516", "184467440737095516.0"))
writeFile(grainsPath, s)

proc remove(repo: ProbSpecsRepo) =
removeDir(repo.dir)

Expand Down Expand Up @@ -88,8 +78,20 @@ proc initProbSpecsTestCases(node: JsonNode): seq[ProbSpecsTestCase] =
for childNode in node["cases"].getElems():
result.add(initProbSpecsTestCases(childNode))

proc grainsWorkaround(grainsPath: string): JsonNode =
## Parses the canonical data file for `grains`, replacing the too-large
## integers with floats. This avoids an error that otherwise occurs when
## parsing integers are too large to store as a 64-bit signed integer.
let sanitised = readFile(grainsPath).multiReplace(
("92233720368547758", "92233720368547758.0"),
("184467440737095516", "184467440737095516.0"))
result = parseJson(sanitised)

proc parseProbSpecsTestCases(repoExercise: ProbSpecsRepoExercise): seq[ProbSpecsTestCase] =
initProbSpecsTestCases(json.parseFile(repoExercise.canonicalDataFile))
if repoExercise.slug == "grains":
repoExercise.canonicalDataFile().grainsWorkaround().initProbSpecsTestCases()
else:
repoExercise.canonicalDataFile().parseFile().initProbSpecsTestCases()

proc initProbSpecsExercise(repoExercise: ProbSpecsRepoExercise): ProbSpecsExercise =
result.slug = repoExercise.slug
Expand All @@ -100,13 +102,86 @@ proc findProbSpecsExercises(repo: ProbSpecsRepo, conf: Conf): seq[ProbSpecsExerc
if conf.exercise.isNone or conf.exercise.get() == repoExercise.slug:
result.add(initProbSpecsExercise(repoExercise))

proc findProbSpecsExercises*(conf: Conf): seq[ProbSpecsExercise] =
let probSpecsRepo = initProbSpecsRepo()

template withDir(dir: string; body: untyped): untyped =
## Changes the current directory to `dir` temporarily.
let startDir = getCurrentDir()
try:
probSpecsRepo.remove()
probSpecsRepo.clone()
probSpecsRepo.grainsWorkaround()
probSpecsRepo.findProbSpecsExercises(conf)
setCurrentDir(dir)
body
finally:
probSpecsRepo.remove()
setCurrentDir(startDir)

proc getNameOfRemote(probSpecsDir, location: string): string =
## Returns the name of the remote in `probSpecsDir` that points to `location`.
##
## Raises an error if there is no remote that points to `location`.
# There's probably a better way to do this than parsing `git remote -v`.
let (remotes, errRemotes) = execCmdEx("git remote -v")
if errRemotes != 0:
showError("could not run `git remote -v` in the given " &
&"problem-specifications directory: '{probSpecsDir}'")
var remoteName, remoteUrl: string
for line in remotes.splitLines():
discard line.scanf("$s$w$s$+fetch)$.", remoteName, remoteUrl)
if remoteUrl.contains(location):
return remoteName
showError(&"there is no remote that points to '{location}' in the " &
&"given problem-specifications directory: '{probSpecsDir}'")

proc validate(probSpecsRepo: ProbSpecsRepo) =
## Raises an error if the given `probSpecsRepo` is not a valid
## `problem-specifications` repo that is up-to-date with upstream.
const mainBranchName = "master"

let probSpecsDir = probSpecsRepo.dir
logDetailed(&"Using user-provided problem-specifications dir: {probSpecsDir}")

# Exit if the given directory does not exist.
if not dirExists(probSpecsDir):
showError("the given problem-specifications directory does not exist: " &
&"'{probSpecsDir}'")

withDir probSpecsDir:
# Exit if the given directory is not a git repo.
if execCmd("git rev-parse") != 0:
showError("the given problem-specifications directory is not a git " &
&"repository: '{probSpecsDir}'")

# Exit if the working directory is not clean.
if execCmd("git diff-index --quiet HEAD") != 0: # Ignores untracked files.
showError("the given problem-specifications working directory is not " &
&"clean: '{probSpecsDir}'")

# Find the name of the remote that points to upstream. Don't assume the
# remote is called 'upstream'.
# Exit if the repo has no remote that points to upstream.
const upstreamLocation = "github.com/exercism/problem-specifications"
let remoteName = getNameOfRemote(probSpecsDir, upstreamLocation)

# For now, just exit with an error if the HEAD is not up-to-date with
# upstream, even if it's possible to do a fast-forward merge.
if execCmd(&"git fetch --quiet {remoteName} {mainBranchName}") != 0:
showError(&"failed to fetch `{mainBranchName}` in " &
&"problem-specifications directory: '{probSpecsDir}'")

# Allow HEAD to be on a non-`master` branch, as long as it's up-to-date
# with `upstream/master`.
let (revHead, _) = execCmdEx("git rev-parse HEAD")
let (revUpstream, _) = execCmdEx(&"git rev-parse {remoteName}/{mainBranchName}")
if revHead != revUpstream:
showError("the given problem-specifications directory is not " &
&"up-to-date: '{probSpecsDir}'")

proc findProbSpecsExercises*(conf: Conf): seq[ProbSpecsExercise] =
if conf.probSpecsDir.isSome():
let probSpecsRepo = ProbSpecsRepo(dir: conf.probSpecsDir.get())
probSpecsRepo.validate()
result = probSpecsRepo.findProbSpecsExercises(conf)
else:
let probSpecsRepo = initProbSpecsRepo()
try:
probSpecsRepo.remove()
probSpecsRepo.clone()
result = probSpecsRepo.findProbSpecsExercises(conf)
finally:
probSpecsRepo.remove()
3 changes: 3 additions & 0 deletions tests/all_tests.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ../tests/[
test_probspecs,
]
1 change: 1 addition & 0 deletions tests/config.nims
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
switch("path", "$projectDir/../src")
61 changes: 61 additions & 0 deletions tests/test_probspecs.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# This module contains tests for `src/probspecs.nim`
import std/[json, options, os, osproc, strformat, unittest]
import cli, probspecs

type
ProblemSpecsDir = enum
psFresh = "fresh clone"
psExisting = "existing dir (simulate the `--probSpecsDir` option)"

proc main =
let existingDir = getTempDir() / "test_probspecs_problem-specifications"
removeDir(existingDir)

for ps in ProblemSpecsDir:
suite &"findProbSpecsExercises: {ps}":
if ps == psExisting:
let cmd = "git clone --depth 1 --quiet " &
"https://github.com/exercism/problem-specifications/ " &
existingDir
test "can make our own clone for later use as an \"existing dir\"":
check:
execCmd(cmd) == 0

let probSpecsDir =
case ps
of psFresh: none(string)
of psExisting: some(existingDir)

let conf = Conf(probSpecsDir: probSpecsDir)
let probSpecsExercises = findProbSpecsExercises(conf)

test "can return the exercises":
check:
probSpecsExercises.len >= 116

test "the first exercise is as expected":
let exercise = probSpecsExercises[0]

check:
exercise.slug == "acronym" # The first exercise with canonical data.
exercise.testCases.len >= 9 # Tests are never removed.

test "the first test case is as expected":
let firstTestCase = probSpecsExercises[0].testCases[0].json
let firstTestCaseExpected = """{
"uuid": "1e22cceb-c5e4-4562-9afe-aef07ad1eaf4",
"description": "basic",
"property": "abbreviate",
"input": {
"phrase": "Portable Network Graphics"
},
"expected": "PNG"
}""".parseJson()

check:
firstTestCase == firstTestCaseExpected

removeDir(existingDir)

main()
{.used.}