From 66914845a323d2e21ab7d8b00b7219d0777f4c17 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 5 Oct 2023 15:42:32 +0200 Subject: [PATCH] fmt: support formatting track config (#772) And add integration tests for `fmt`. Refs: #478 Closes: #479 --- .github/workflows/tests.yml | 5 + src/exec.nim | 19 +++ src/fmt/fmt.nim | 49 +++++-- src/fmt/track_config.nim | 269 ++++++++++++++++++++++++++++++++++ src/info/info.nim | 8 +- src/sync/sync.nim | 1 + src/types_track_config.nim | 80 +++++++++- tests/test_binary.nim | 281 ++++++++++++++++++++++++++++++++++++ 8 files changed, 685 insertions(+), 27 deletions(-) create mode 100644 src/fmt/track_config.nim diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 932b6006..cb31f46b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,6 +37,11 @@ jobs: - name: Checkout code uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + - name: Configure the git user # Required to create a commit in our binary tests + run: | + git config --global user.email "66069679+exercism-bot@users.noreply.github.com" + git config --global user.name "Exercism Bot" + - name: On Linux, install musl if: matrix.os == 'linux' run: ./.github/bin/linux-install-musl diff --git a/src/exec.nim b/src/exec.nim index 61e89777..38733901 100644 --- a/src/exec.nim +++ b/src/exec.nim @@ -108,6 +108,20 @@ proc gitCheckout(dir, hash: string) = let args = ["-C", dir, "checkout", "--force", hash] discard gitCheck(0, args) +proc fixAverageRunTimeInConfigJson(file, oldValue, newValue: string) = + ## For testing purposes, we clone track repos and checkout commits + ## where the `average_run_time` value in the `config.json` file + ## is a float, which has later since changed to an int. + ## In order to not break existing tests, we manually change the value + ## from a float to an int. + let configJson = readFile(file) + .replace( + &""""average_run_time": {oldValue}""", + &""""average_run_time": {newValue}""") + writeFile(file, configJson) + let args = ["-C", parentDir(file), "commit", "-a", "-m", "config: convert `average_run_time` to int"] + discard gitCheck(0, args) + proc setupExercismRepo*(repoName, dest, hash: string; shallow = false) = ## If there is no directory at `dest`, clones the Exercism repo named ## `repoName` to `dest`. @@ -117,6 +131,11 @@ proc setupExercismRepo*(repoName, dest, hash: string; shallow = false) = cloneExercismRepo(repoName, dest, shallow) gitCheckout(dest, hash) + if repoName == "nim": + fixAverageRunTimeInConfigJson(dest / "config.json", "3.0", "3") + elif repoName == "elixir": + fixAverageRunTimeInConfigJson(dest / "config.json", "4.1", "4") + func conciseDiff(s: string): string = ## Returns the lines of `s` that begin with a `+` or `-` character. result = newStringOfCap(s.len) diff --git a/src/fmt/fmt.nim b/src/fmt/fmt.nim index 110bc827..4b955ccb 100644 --- a/src/fmt/fmt.nim +++ b/src/fmt/fmt.nim @@ -1,11 +1,13 @@ import std/[os, strformat, strutils] -import "."/[approaches, articles, exercises] +import "."/[approaches, articles, exercises, track_config] import ".."/[cli, helpers, logger, sync/sync_common, sync/sync, types_exercise_config, types_track_config] type DocumentKind* = enum - dkExerciseConfig, + dkTrackConfig, + dkConceptExerciseConfig, + dkPracticeExerciseConfig, dkApproachesConfig, dkArticlesConfig @@ -15,11 +17,23 @@ type formattedDocument: string iterator getConfigPaths(trackExerciseSlugs: TrackExerciseSlugs, - trackExercisesDir: string): (ExerciseKind, DocumentKind, string) = + conf: Conf): (DocumentKind, string) = + let trackDir = conf.trackDir + + ## Yield the track's `config.json` file, but only when the user + ## is not formatting a single exercise + if conf.action.exerciseFmt.len == 0: + yield (dkTrackConfig, trackDir / "config.json") + ## Yields the `.meta/config.json`, `.approaches/config.json` and ## `.articles/config.json` paths for each exercise in ## `trackExerciseSlugs` in `trackExercisesDir`. + let trackExercisesDir = trackDir / "exercises" for exerciseKind in [ekConcept, ekPractice]: + let documentKind = + case exerciseKind + of ekConcept: dkConceptExerciseConfig + of ekPractice: dkPracticeExerciseConfig let slugs = case exerciseKind of ekConcept: trackExerciseSlugs.`concept` @@ -34,34 +48,37 @@ iterator getConfigPaths(trackExerciseSlugs: TrackExerciseSlugs, for slug in slugs: trackExerciseConfigPath.truncateAndAdd(startLen, slug) trackExerciseConfigPath.addExerciseConfigPath() - yield (exerciseKind, dkExerciseConfig, trackExerciseConfigPath) + yield (documentKind, trackExerciseConfigPath) trackExerciseConfigPath.truncateAndAdd(startLen, slug) trackExerciseConfigPath.addApproachesConfigPath() if fileExists(trackExerciseConfigPath): - yield (exerciseKind, dkApproachesConfig, trackExerciseConfigPath) + yield (dkApproachesConfig, trackExerciseConfigPath) trackExerciseConfigPath.truncateAndAdd(startLen, slug) trackExerciseConfigPath.addArticlesConfigPath() if fileExists(trackExerciseConfigPath): - yield (exerciseKind, dkArticlesConfig, trackExerciseConfigPath) + yield (dkArticlesConfig, trackExerciseConfigPath) proc fmtImpl(trackExerciseSlugs: TrackExerciseSlugs, - trackDir: string): seq[PathAndFormattedDocument] = - ## Reads the config files for every slug in `trackExerciseSlugs` - ## in `trackExerciseDir`. + conf: Conf): seq[PathAndFormattedDocument] = + ## Reads the track config file and all exercise config files + ## for every slug in `trackExerciseSlugs` in `trackExerciseDir`. ## This includes `.meta/config.json`, `.approaches/config.json` - ## and `.articles/config.json`. + ## and `.articles/config.json` for each exercise, and `config.json` + ## for the track. ## ## Returns a seq of (document kind, path, formatted document) objects - ## containing every exercise's configs that are not already formatted. - let trackExercisesDir = trackDir / "exercises" + ## containing every document that is not already formatted. var seenUnformatted = false - for (exerciseKind, documentKind, configPath) in getConfigPaths(trackExerciseSlugs, - trackExercisesDir): + let trackDir = conf.trackDir + for (documentKind, configPath) in getConfigPaths(trackExerciseSlugs, + conf): let formatted = case documentKind - of dkExerciseConfig: formatExerciseConfigFile(exerciseKind, configPath) + of dkTrackConfig: formatTrackConfigFile(configPath) + of dkConceptExerciseConfig: formatExerciseConfigFile(ekConcept, configPath) + of dkPracticeExerciseConfig: formatExerciseConfigFile(ekPractice, configPath) of dkApproachesConfig: formatApproachesConfigFile(configPath) of dkArticlesConfig: formatArticlesConfigFile(configPath) @@ -116,7 +133,7 @@ proc fmt*(conf: Conf) = logNormal("Looking for exercises that lack a formatted '.meta/config.json', " & "'.approaches/config.json'\nor '.articles/config.json' file...") - let pairs = fmtImpl(trackExerciseSlugs, conf.trackDir) + let pairs = fmtImpl(trackExerciseSlugs, conf) let userExercise = conf.action.exerciseFmt if pairs.len > 0: diff --git a/src/fmt/track_config.nim b/src/fmt/track_config.nim new file mode 100644 index 00000000..f4c5db57 --- /dev/null +++ b/src/fmt/track_config.nim @@ -0,0 +1,269 @@ +import std/[algorithm, sequtils, json, options, sets, strformat] +import pkg/jsony +import ".."/[helpers, sync/sync_common, types_track_config] + +func trackConfigKeyOrderForFmt(e: TrackConfig): seq[TrackConfigKey] = + result = @[] + if e.language.len > 0: + result.add tckLanguage + if e.slug.len > 0: + result.add tckSlug + result.add tckActive + result.add tckStatus + if e.blurb.len > 0: + result.add tckBlurb + result.add tckVersion + result.add tckOnlineEditor + if e.testRunner.averageRunTime > 0: + result.add tckTestRunner + if e.files.solution.len > 0 or + e.files.test.len > 0 or + e.files.exemplar.len > 0 or + e.files.example.len > 0 or + e.files.editor.len > 0 or + e.files.invalidator.len > 0: + result.add tckFiles + if e.exercises.`concept`.len > 0 or + e.exercises.practice.len > 0 or + e.exercises.foregone.len > 0: + result.add tckExercises + if e.concepts.len > 0: + result.add tckConcepts + if e.keyFeatures.len > 0: + result.add tckKeyFeatures + if e.tags.len > 0: + result.add tckTags + +func addStatus(result: var string; val: TrackStatus; indentLevel = 1) = + result.addNewlineAndIndent(indentLevel) + escapeJson("status", result) + result.add ": {" + result.addBool("concept_exercises", val.conceptExercises, + indentLevel = indentLevel + 1) + result.addBool("test_runner", val.testRunner, indentLevel = indentLevel + 1) + result.addBool("representer", val.representer, indentLevel = indentLevel + 1) + result.addBool("analyzer", val.analyzer, indentLevel = indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addTestRunner(result: var string; val: TestRunner; indentLevel = 1) = + result.addNewlineAndIndent(indentLevel) + escapeJson("test_runner", result) + result.add ": {" + result.addInt("average_run_time", val.averageRunTime, + indentLevel = indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addOnlineEditor(result: var string; val: OnlineEditor; indentLevel = 1) = + result.addNewlineAndIndent(indentLevel) + escapeJson("online_editor", result) + result.add ": {" + result.addString("indent_style", $val.indentStyle, indentLevel = indentLevel + 1) + result.addInt("indent_size", val.indentSize, indentLevel = indentLevel + 1) + if val.highlightjsLanguage.len > 0: + result.addString("highlightjs_language", val.highlightjsLanguage, + indentLevel = indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addFiles(result: var string; val: FilePatterns; indentLevel = 1) = + result.addNewlineAndIndent(indentLevel) + escapeJson("files", result) + result.add ": {" + if val.solution.len > 0: + result.addArray("solution", val.solution, indentLevel = indentLevel + 1) + if val.test.len > 0: + result.addArray("test", val.test, indentLevel = indentLevel + 1) + if val.example.len > 0: + result.addArray("example", val.example, indentLevel = indentLevel + 1) + if val.exemplar.len > 0: + result.addArray("exemplar", val.exemplar, indentLevel = indentLevel + 1) + if val.editor.len > 0: + result.addArray("editor", val.editor, indentLevel = indentLevel + 1) + if val.invalidator.len > 0: + result.addArray("invalidator", val.invalidator, indentLevel = indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addTags(result: var string; val: OrderedSet[string]; indentLevel = 1) = + ## Appends the pretty-printed JSON for an `val` key with value `tags` + ## to `result`. + var tags = toSeq(val) + sort tags + + result.addArray("tags", tags, indentLevel) + +func addKeyFeature(result: var string; val: KeyFeature; indentLevel = 1) = + ## Appends the pretty-printed JSON for an `key_feature` object with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + result.add "{" + result.addString("title", val.title, indentLevel + 1) + result.addString("content", val.content, indentLevel + 1) + result.addString("icon", val.icon, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addKeyFeatures(result: var string; val: KeyFeatures; indentLevel = 1) = + ## Appends the pretty-printed JSON for an `key_features` key with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + escapeJson("key_features", result) + result.add ": [" + for keyFeature in val: + result.addKeyFeature(keyFeature, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "]," + +func addConcept(result: var string; val: Concept; indentLevel = 1) = + ## Appends the pretty-printed JSON for a `concept` object with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + result.add "{" + result.addString("uuid", val.uuid, indentLevel + 1) + result.addString("slug", val.slug, indentLevel + 1) + result.addString("name", val.name, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addConceptExercise(result: var string; val: ConceptExercise; + indentLevel = 1) = + ## Appends the pretty-printed JSON for a `concept` object with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + result.add "{" + result.addString("slug", $val.slug, indentLevel + 1) + result.addString("name", val.name, indentLevel + 1) + result.addString("uuid", val.uuid, indentLevel + 1) + result.addArray("concepts", toSeq(val.concepts), indentLevel + 1) + result.addArray("prerequisites", toSeq(val.prerequisites), indentLevel + 1) + if val.status != sMissing: + result.addString("status", $val.status, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addConceptExercises(result: var string; val: seq[ConceptExercise]; + indentLevel = 2) = + ## Appends the pretty-printed JSON for a `concepts` key with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + escapeJson("concept", result) + result.add ": [" + for exercise in val: + result.addConceptExercise(exercise, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "]," + +func addPracticeExercise(result: var string; val: PracticeExercise; + indentLevel = 1) = + ## Appends the pretty-printed JSON for a `practice` object with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + result.add "{" + result.addString("slug", $val.slug, indentLevel + 1) + result.addString("name", val.name, indentLevel + 1) + result.addString("uuid", val.uuid, indentLevel + 1) + result.addArray("practices", toSeq(val.practices), indentLevel + 1) + result.addArray("prerequisites", toSeq(val.prerequisites), indentLevel + 1) + result.addInt("difficulty", val.difficulty, indentLevel + 1) + if val.status != sMissing: + result.addString("status", $val.status, indentLevel + 1) + if val.topics.isSome() and val.topics.get.len > 0: + result.addArray("topics", toSeq(val.topics.get), indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addPracticeExercises(result: var string; val: seq[PracticeExercise]; + indentLevel = 2) = + ## Appends the pretty-printed JSON for a `practice` key with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + escapeJson("practice", result) + result.add ": [" + for exercise in val: + result.addPracticeExercise(exercise, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "]," + +func addExercises(result: var string; val: Exercises; indentLevel = 1) = + ## Appends the pretty-printed JSON for a `concepts` key with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + escapeJson("exercises", result) + result.add ": {" + if val.`concept`.len > 0: + result.addConceptExercises(val.`concept`, indentLevel + 1) + if val.practice.len > 0: + result.addPracticeExercises(val.practice, indentLevel + 1) + if val.foregone.len > 0: + result.addArray("foregone", toSeq(val.foregone), indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "}," + +func addConcepts(result: var string; val: Concepts; indentLevel = 1) = + ## Appends the pretty-printed JSON for a `concepts` key with value `val` to + ## `result`. + result.addNewlineAndIndent(indentLevel) + escapeJson("concepts", result) + result.add ": [" + for `concept` in val: + result.addConcept(`concept`, indentLevel + 1) + result.removeComma() + result.addNewlineAndIndent(indentLevel) + result.add "]," + +func prettyTrackConfig(e: TrackConfig): string = + ## Serializes `e` as pretty-printed JSON, using the canonical key order. + let keys = trackConfigKeyOrderForFmt(e) + + result = newStringOfCap(2000) + result.add '{' + for key in keys: + case key + of tckLanguage: + result.addString("language", e.language) + of tckSlug: + result.addString("slug", e.slug) + of tckBlurb: + result.addString("blurb", e.blurb) + of tckActive: + result.addBool("active", e.active) + of tckVersion: + result.addInt("version", e.version) + of tckExercises: + result.addExercises(e.exercises) + of tckFiles: + result.addFiles(e.files) + of tckConcepts: + result.addConcepts(e.concepts) + of tckOnlineEditor: + result.addOnlineEditor(e.onlineEditor) + of tckKeyFeatures: + result.addKeyFeatures(e.keyFeatures) + of tckTestRunner: + result.addTestRunner(e.testRunner) + of tckStatus: + result.addStatus(e.status) + of tckTags: + result.addTags(e.tags) + result.removeComma() + result.add "\n}\n" + +proc formatTrackConfigFile*(configPath: string): string = + ## Parses the `config.json` file at `configPath` and returns it in the + ## canonical form. + let trackConfig = TrackConfig.init configPath.readFile() + prettyTrackConfig(trackConfig) diff --git a/src/info/info.nim b/src/info/info.nim index 90fe8c0f..fac77eea 100644 --- a/src/info/info.nim +++ b/src/info/info.nim @@ -73,15 +73,17 @@ proc init(T: typedesc[ProbSpecsExercises], probSpecsDir: ProbSpecsDir): T = result.withoutCanonicalData.incl exerciseSlug proc unimplementedProbSpecsExercises(practiceExercises: seq[PracticeExercise], - foregone: HashSet[string], + foregone: OrderedSet[string], probSpecsDir: ProbSpecsDir): string = let probSpecsExercises = ProbSpecsExercises.init(probSpecsDir) practiceExerciseSlugs = collect: for p in practiceExercises: {p.slug.`$`} - uWith = probSpecsExercises.withCanonicalData - practiceExerciseSlugs - foregone - uWithout = probSpecsExercises.withoutCanonicalData - practiceExerciseSlugs - foregone + uWith = probSpecsExercises.withCanonicalData - practiceExerciseSlugs - + foregone.toSeq.toHashSet + uWithout = probSpecsExercises.withoutCanonicalData - practiceExerciseSlugs - + foregone.toSeq.toHashSet header = &"There are {uWith.len + uWithout.len} non-deprecated exercises " & "in `exercism/problem-specifications` that\n" & diff --git a/src/sync/sync.nim b/src/sync/sync.nim index 99de5013..e3d0c6e8 100644 --- a/src/sync/sync.nim +++ b/src/sync/sync.nim @@ -1,4 +1,5 @@ import std/[os, sequtils, strformat, strutils, terminal] +import pkg/jsony # This is not always used, but removing it will make tests fail. import ".."/[cli, helpers, logger, types_track_config] import "."/[exercises, probspecs, sync_common, sync_docs, sync_filepaths, sync_metadata, sync_tests] diff --git a/src/types_track_config.nim b/src/types_track_config.nim index 76f92448..bcf06a65 100644 --- a/src/types_track_config.nim +++ b/src/types_track_config.nim @@ -1,10 +1,34 @@ -import std/[hashes, sets] +import std/[hashes, options, sets] import pkg/jsony import "."/[cli, helpers] type Slug* = distinct string ## A `slug` value in a track `config.json` file is a kebab-case string. +type + FilePatternsKey* = enum + fpSolution = "solution" + fpTest = "test" + fpExemplar = "exemplar" + fpExample = "example" + fpEditor = "editor" + fpInvalidator = "invalidator" + + TrackConfigKey* = enum + tckLanguage = "language" + tckSlug = "string" + tckActive = "active" + tckBlurb = "blurb" + tckVersion = "version" + tckExercises = "exercises" + tckFiles = "files" + tckConcepts = "concepts" + tckTestRunner = "test_runner" + tckOnlineEditor = "online_editor" + tckKeyFeatures = "key_features" + tckStatus = "status" + tckTags = "tags" + Status* = enum sMissing = "missing" sWip = "wip" @@ -12,25 +36,31 @@ type sActive = "active" sDeprecated = "deprecated" - # We can use a `HashSet` for `concepts`, `prerequisites`, `practices`, and + # We can use an `OrderedSet` for `concepts`, `prerequisites`, `practices`, and # `foregone` because the first pass has already checked that each has unique - # values. + # values, but we want to retain insertion order to reduce churn. ConceptExercise* = object slug*: Slug - concepts*: HashSet[string] - prerequisites*: HashSet[string] + name*: string + uuid*: string + concepts*: OrderedSet[string] + prerequisites*: OrderedSet[string] status*: Status PracticeExercise* = object slug*: Slug - practices*: HashSet[string] - prerequisites*: HashSet[string] + name*: string + uuid*: string + practices*: OrderedSet[string] + prerequisites*: OrderedSet[string] + difficulty*: int + topics*: Option[OrderedSet[string]] status*: Status Exercises* = object `concept`*: seq[ConceptExercise] practice*: seq[PracticeExercise] - foregone*: HashSet[string] + foregone*: OrderedSet[string] FilePatterns* = object solution*: seq[string] @@ -40,6 +70,15 @@ type editor*: seq[string] invalidator*: seq[string] + IndentStyle* = enum + isSpace = "space" + isTab = "tab" + + OnlineEditor* = object + indentStyle*: IndentStyle + indentSize*: int + highlightjsLanguage*: string + Concept* = object name*: string slug*: string @@ -47,11 +86,36 @@ type Concepts* = seq[Concept] + KeyFeature* = object + icon*: string + title*: string + content*: string + + KeyFeatures* = seq[KeyFeature] + + TestRunner* = object + averageRunTime*: int + + TrackStatus* = object + conceptExercises*: bool + testRunner*: bool + representer*: bool + analyzer*: bool + TrackConfig* = object + language*: string slug*: string + active*: bool + blurb*: string + version*: int exercises*: Exercises files*: FilePatterns concepts*: Concepts + testRunner*: TestRunner + onlineEditor*: OnlineEditor + keyFeatures*: KeyFeatures + status*: TrackStatus + tags*: OrderedSet[string] func `$`*(slug: Slug): string {.borrow.} func `==`*(x, y: Slug): bool {.borrow.} diff --git a/tests/test_binary.nim b/tests/test_binary.nim index be4f646b..0ba9a3dc 100644 --- a/tests/test_binary.nim +++ b/tests/test_binary.nim @@ -1008,6 +1008,285 @@ proc testsForCompletion(binaryPath: string) = outp.contains(&"Please choose a shell. For example: `configlet completion -s bash`") exitCode == 1 +proc testsForFmt(binaryPath: static string) = + const formattedTrackDir = testsDir / ".test_nim_track_repo" + const unformattedTrackDir = testsDir / ".test_elixir_track_repo" + + # Setup: clone track repo, and checkout a known state (formatted) + setupExercismRepo("nim", formattedTrackDir, + "ea91acb3edb6c7bc05dd3b050c0a566be6c3329e") # 2022-01-22 + + # Setup: clone track repo, and checkout a known state (unformatted) + setupExercismRepo("elixir", unformattedTrackDir, + "07448c4f870c15f8191196a2b01e8bf09708b8ce") # 2022-01-11 + + const + fmtBaseUnformatted = &"{binaryPath} -t {unformattedTrackDir} fmt" + fmtBaseFormatted = &"{binaryPath} -t {formattedTrackDir} fmt" + fmtUpdateUnformatted = &"{fmtBaseUnformatted} --update" + fmtUpdateFormatted = &"{fmtBaseFormatted} --update" + configJsonAbsolutePathUnFormatted = unformattedTrackDir / "config.json" + unformattedHeader = &"Found 39 Concept Exercises and 118 Practice Exercises in {configJsonAbsolutePathUnFormatted}" + + suite "fmt, when the track `config.json` file is not formatted": + test "prints the expected output, and exits with 1": + const expectedOutput = fmt""" + {unformattedHeader} + Looking for exercises that lack a formatted '.meta/config.json', '.approaches/config.json' + or '.articles/config.json' file... + The below paths are relative to '{unformattedTrackDir}' + Not formatted: config.json + Not formatted: {"exercises"/"concept"/"basketball-website"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"bird-count"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"boutique-inventory"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"boutique-suggestions"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"bread-and-potions"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"captains-log"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"chessboard"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"city-office"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"community-garden"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"date-parser"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"dna-encoding"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"file-sniffer"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"freelancer-rates"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"german-sysadmin"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"guessing-game"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"high-school-sweetheart"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"high-score"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"kitchen-calculator"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"language-list"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"lasagna"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"library-fees"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"log-level"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"lucas-numbers"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"mensch-aergere-dich-nicht"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"name-badge"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"need-for-speed"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"new-passport"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"newsletter"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"pacman-rules"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"remote-control-car"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"rpg-character-sheet"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"rpn-calculator"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"rpn-calculator-inspection"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"rpn-calculator-output"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"secrets"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"stack-underflow"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"take-a-number"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"top-secret"/".meta"/"config.json"} + Not formatted: {"exercises"/"concept"/"wine-cellar"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"accumulate"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"acronym"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"affine-cipher"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"all-your-base"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"allergies"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"alphametics"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"anagram"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"armstrong-numbers"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"atbash-cipher"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"bank-account"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"beer-song"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"binary-search"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"binary-search-tree"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"bob"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"book-store"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"bowling"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"change"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"circular-buffer"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"clock"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"collatz-conjecture"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"complex-numbers"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"connect"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"crypto-square"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"custom-set"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"darts"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"diamond"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"difference-of-squares"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"diffie-hellman"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"dnd-character"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"dominoes"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"dot-dsl"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"etl"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"flatten-array"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"food-chain"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"forth"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"gigasecond"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"go-counting"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"grade-school"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"grains"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"grep"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"hamming"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"hello-world"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"house"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"isbn-verifier"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"isogram"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"kindergarten-garden"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"knapsack"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"largest-series-product"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"leap"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"list-ops"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"luhn"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"markdown"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"matching-brackets"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"matrix"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"meetup"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"minesweeper"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"nth-prime"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"nucleotide-count"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"ocr-numbers"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"palindrome-products"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"pangram"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"parallel-letter-frequency"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"pascals-triangle"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"perfect-numbers"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"phone-number"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"pig-latin"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"poker"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"pov"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"prime-factors"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"protein-translation"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"proverb"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"pythagorean-triplet"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"queen-attack"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"rail-fence-cipher"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"raindrops"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"rational-numbers"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"react"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"rectangles"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"resistor-color"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"resistor-color-duo"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"resistor-color-trio"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"rna-transcription"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"robot-simulator"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"roman-numerals"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"rotational-cipher"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"run-length-encoding"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"saddle-points"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"satellite"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"say"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"scale-generator"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"scrabble-score"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"secret-handshake"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"series"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"sgf-parsing"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"sieve"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"simple-cipher"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"simple-linked-list"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"space-age"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"spiral-matrix"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"square-root"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"strain"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"sublist"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"sum-of-multiples"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"tournament"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"transpose"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"triangle"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"twelve-days"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"two-bucket"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"two-fer"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"variable-length-quantity"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"word-count"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"word-search"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"wordy"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"yacht"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"zebra-puzzle"/".meta"/"config.json"} + Not formatted: {"exercises"/"practice"/"zipper"/".meta"/"config.json"} + """.unindent().replace("\p", "\n") + let cmd = fmtBaseUnformatted + execAndCheck(1, cmd, expectedOutput) + + suite "fmt, for an exercise that is not formatted (prints the expected output, and exits with 1)": + test "-e bob": + const expectedOutput = fmt""" + {unformattedHeader} + Looking for exercises that lack a formatted '.meta/config.json', '.approaches/config.json' + or '.articles/config.json' file... + The below paths are relative to '{unformattedTrackDir}' + Not formatted: {"exercises"/"practice"/"bob"/".meta"/"config.json"} + """.unindent().replace("\p", "\n") + execAndCheck(1, &"{fmtBaseUnformatted} -e bob", expectedOutput) + + suite "fmt, for an exercise that does not exist (prints the expected output, and exits with 1)": + test "-e foo": + const expectedOutput = fmt""" + {unformattedHeader} + The `-e, --exercise` option was used to specify an exercise slug, but `foo` is not an slug in the track config: + {unformattedTrackDir / "config.json"} + """.unindent().replace("\p", "\n") + execAndCheck(1, &"{fmtBaseUnformatted} -e foo", expectedOutput) + + suite "fmt, with --update, without --yes, for an exercise that is not formatted (no diff, and exits with 1)": + test "-e bob": + let exitCode = execCmdEx(&"{fmtUpdateUnformatted} -e bob")[1] + check exitCode == 1 + checkNoDiff(unformattedTrackDir) + + suite "fmt, with --update, for an exercise that is not formatted (diff, and exits with 0)": + test "-e bob": + let exitCode = execCmdEx(&"{fmtUpdateUnformatted} --yes -e bob")[1] + check exitCode == 0 + const expectedDiff = """ + --- exercises/practice/bob/.meta/config.json + +++ exercises/practice/bob/.meta/config.json + - "blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited.", + + "blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited.", + """.unindent().replace("\p", "\n") + let configPath = "exercises" / "practice" / "bob" / ".meta" / "config.json" + let trackDir = unformattedTrackDir + testDiffThenRestore(trackDir, expectedDiff, configPath) + + suite "fmt, with --update --yes, for an exercise that is formatted (no diff, and exits with 0)": + test "-e bob": + let (outp, exitCode) = execCmdEx(&"{fmtUpdateFormatted} --yes -e bob") + check exitCode == 0 + checkNoDiff(formattedTrackDir) + + suite "fmt, with --update, for an exercise that is formatted (no diff, and exits with 0)": + test "-e bob": + let exitCode = execCmdEx(&"{fmtUpdateFormatted} --yes -e bob")[1] + check exitCode == 0 + checkNoDiff(formattedTrackDir) + + suite "fmt, with --update, for an exercise that is not formatted (diff, and exits with 0)": + test "-e bob": + let exitCode = execCmdEx(&"{fmtUpdateUnformatted} --yes -e bob")[1] + check exitCode == 0 + const expectedDiff = """ + --- exercises/practice/bob/.meta/config.json + +++ exercises/practice/bob/.meta/config.json + - "blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited.", + + "blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited.", + """.unindent().replace("\p", "\n") + let configPath = "exercises" / "practice" / "bob" / ".meta" / "config.json" + let trackDir = unformattedTrackDir + testDiffThenRestore(trackDir, expectedDiff, configPath) + + suite "fmt, with --update (diff, and exits with 0)": + test "all": + let exitCode = execCmdEx(&"{fmtUpdateFormatted} --yes ")[1] + check exitCode == 0 + const expectedDiff = """ + --- config.json + +++ config.json + - "concept": [], + - "topics": null, + - "concepts": [], + - "key_features": [], + + "execution_mode/compiled", + - "typing/static", + - "typing/strong", + - "execution_mode/compiled", + - "platform/windows", + - "platform/mac", + + "platform/mac", + + "platform/windows", + + "typing/static", + + "typing/strong", + """.unindent().replace("\p", "\n") + let configPath = "config.json" + let trackDir = formattedTrackDir + testDiffThenRestore(trackDir, expectedDiff, configPath) + proc main = const binaryExt = @@ -1162,5 +1441,7 @@ proc main = testsForCompletion(binaryPath) + testsForFmt(binaryPath) + main() {.used.}