From 6e76aa4fa61704ddf18f1d4ebf0d3b43fd781e99 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 19 Apr 2024 19:22:40 +0200 Subject: [PATCH 1/6] Support specifying the author and/or difficulty when creating exercises. --- README.md | 2 ++ src/cli.nim | 17 ++++++++++++++++- src/create/create.nim | 15 ++++++++++++++- src/create/exercises.nim | 23 +++++++++++++++++++---- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b5a3c5f5..7cf2c268 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ Options for create: --concept-exercise The slug of the concept exercise -e, --exercise Only operate on this exercise -o, --offline Do not update the cached 'problem-specifications' data + -a, --author The author of the exercise, approach or article + -d, --difficult The difficulty of the exercise (default: 1) Options for fmt: -e, --exercise Only operate on this exercise diff --git a/src/cli.nim b/src/cli.nim index 50ed3272..675e5cf6 100644 --- a/src/cli.nim +++ b/src/cli.nim @@ -1,4 +1,4 @@ -import std/[os, parseutils, strformat, strutils, terminal] +import std/[options, os, parseutils, strformat, strutils, terminal] import pkg/supersnappy import patched_libs/parseopt3 @@ -47,6 +47,8 @@ type # in object variants. exerciseCreate*: string offlineCreate*: bool + author*: string + difficulty*: Option[int] of actFmt: # We can't name these fields `exercise`, `update`, and `yes` because we # use those names in `actSync`, and Nim doesn't yet support duplicate @@ -88,6 +90,8 @@ type optCreateArticle = "article" optCreateConceptExercise = "conceptExercise" optCreatePracticeExercise = "practiceExercise" + optCreateAuthor = "author" + optCreateDifficulty = "difficulty" # Options for `completion` optCompletionShell = "shell" @@ -244,6 +248,8 @@ func genHelpText: string = optCreateArticle: "The slug of the article", optCreateConceptExercise: "The slug of the concept exercise", optCreatePracticeExercise: "The slug of the practice exercise", + optCreateAuthor: "The author of the exercise, approach or article", + optCreateDifficulty: "The difficulty of the exercise (default: 1)", optCompletionShell: &"Choose the shell type (required)\n" & &"{paddingOpt}{allowedValues(Shell)}", optFmtSyncCreateExercise: "Only operate on this exercise", @@ -552,6 +558,15 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) = setActionOpt(practiceExerciseSlug, val) of optInfoSyncCreateOffline: setActionOpt(offlineCreate, true) + of optCreateAuthor: + setActionOpt(author, val) + of optCreateDifficulty: + var num = -1 + discard parseSaturatedNatural(val, num) + if num notin 1..10: + showError(&"value for {formatOpt(kind, key)} is not between " & + &" 1 to 10: {val}") + setActionOpt(difficulty, some(num)) else: discard of actFmt: diff --git a/src/create/create.nim b/src/create/create.nim index cfcbc951..c9b01f7d 100644 --- a/src/create/create.nim +++ b/src/create/create.nim @@ -1,4 +1,4 @@ -import std/[os, strformat] +import std/[options, os, strformat] import ".."/[cli, helpers, sync/sync, types_track_config] import "."/[approaches, articles, exercises] @@ -13,6 +13,10 @@ proc create*(conf: Conf) = let msg = &"Both --approach and --article were provided. Please specify only one." stderr.writeLine msg quit QuitFailure + if conf.action.difficulty.isSome: + let msg = "The difficulty argument is not supported for approaches" + stderr.writeLine msg + quit QuitFailure let trackConfigPath = conf.trackDir / "config.json" let trackConfig = parseFile(trackConfigPath, TrackConfig) let trackExerciseSlugs = getSlugs(trackConfig.exercises, conf, trackConfigPath) @@ -36,6 +40,10 @@ proc create*(conf: Conf) = let msg = "Please specify an exercise to create an article for, using --exercise " stderr.writeLine msg quit QuitFailure + if conf.action.difficulty.isSome: + let msg = "The difficulty argument is not supported for approaches" + stderr.writeLine msg + quit QuitFailure let trackConfigPath = conf.trackDir / "config.json" let trackConfig = parseFile(trackConfigPath, TrackConfig) let trackExerciseSlugs = getSlugs(trackConfig.exercises, conf, trackConfigPath) @@ -55,6 +63,11 @@ proc create*(conf: Conf) = createArticle(Slug(conf.action.articleSlug), userExercise, exerciseDir) elif conf.action.conceptExerciseSlug.len > 0: + if conf.action.difficulty.isSome: + let msg = "The difficulty argument is not supported for concept exercises" + stderr.writeLine msg + quit QuitFailure + createConceptExercise(conf) elif conf.action.practiceExerciseSlug.len > 0: createPracticeExercise(conf) diff --git a/src/create/exercises.nim b/src/create/exercises.nim index 21bc05f0..9988d072 100644 --- a/src/create/exercises.nim +++ b/src/create/exercises.nim @@ -1,7 +1,7 @@ import std/[sets, options, os, strformat] -import ".."/[cli, helpers, logger, fmt/track_config, sync/probspecs, sync/sync, - sync/sync_filepaths, sync/sync_metadata, types_exercise_config, - types_track_config, uuid/uuid] +import ".."/[cli, helpers, logger, fmt/exercises, fmt/track_config, + sync/probspecs, sync/sync, sync/sync_common, sync/sync_filepaths, + sync/sync_metadata, types_exercise_config, types_track_config, uuid/uuid] proc verifyExerciseDoesNotExist(conf: Conf, slug: string): tuple[trackConfig: TrackConfig, trackConfigPath: string, exercise: Slug] = let trackConfigPath = conf.trackDir / "config.json" @@ -53,10 +53,25 @@ proc syncExercise(conf: Conf, slug: Slug,) = ) discard syncImpl(syncConf) +proc setAuthor(conf: Conf, slug: Slug, trackDir: string, exerciseKind: ExerciseKind) = + let configPath = trackDir / "exercises" / $exerciseKind / $slug / ".meta" / "config.json" + var exerciseConfig = ExerciseConfig.init(exerciseKind, configPath) + let formattedConfig = + case exerciseKind + of ekConcept: + exerciseConfig.c.authors.add conf.action.author + prettyExerciseConfig(exerciseConfig.c, pmFmt) + of ekPractice: + exerciseConfig.p.authors.add conf.action.author + prettyExerciseConfig(exerciseConfig.p, pmFmt) + writeFile(configPath, formattedConfig) + proc createFiles(conf: Conf, slug: Slug, trackConfig: TrackConfig, trackDir: string, exerciseKind: ExerciseKind) = withLevel(verQuiet): syncExercise(conf, slug) syncFiles(trackConfig, conf.trackDir, slug, exerciseKind) + if conf.action.author.len > 0: + setAuthor(conf, slug, trackDir, exerciseKind) proc createConceptExercise*(conf: Conf) = var (trackConfig, trackConfigPath, userExercise) = verifyExerciseDoesNotExist(conf, conf.action.conceptExerciseSlug) @@ -105,7 +120,7 @@ proc createPracticeExercise*(conf: Conf) = uuid: $genUuid(), practices: OrderedSet[string](), prerequisites: OrderedSet[string](), - difficulty: 1, + difficulty: conf.action.difficulty.get(1), status: sMissing ) From a23f944656aa086482342c02b29fc0aa39c1b0db Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Mon, 22 Apr 2024 08:46:26 +0200 Subject: [PATCH 2/6] Update usage in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cf2c268..4f6e938e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Options for create: -e, --exercise Only operate on this exercise -o, --offline Do not update the cached 'problem-specifications' data -a, --author The author of the exercise, approach or article - -d, --difficult The difficulty of the exercise (default: 1) + -d, --difficulty The difficulty of the exercise (default: 1) Options for fmt: -e, --exercise Only operate on this exercise From 1318150f451f26edbb544084b40863949881abe0 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Mon, 22 Apr 2024 09:01:31 +0200 Subject: [PATCH 3/6] Add tests --- tests/test_binary_create.nim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_binary_create.nim b/tests/test_binary_create.nim index 7e26c1c3..8f81ebab 100644 --- a/tests/test_binary_create.nim +++ b/tests/test_binary_create.nim @@ -45,6 +45,12 @@ proc main = """.unindent() execAndCheck(1, &"{createBase} --concept-exercise=hangman", expectedOutput) + test "concept exercise with difficulty (prints the expected output, and exits with 1)": + const expectedOutput = fmt""" + The difficulty argument is not supported for concept exercises + """.unindent() + execAndCheck(1, &"{createBase} --concept-exercise=bar --difficulty 4", expectedOutput) + test "create concept exercise (creates the exercise files, and exits with 0)": const expectedOutput = fmt""" Updating cached 'problem-specifications' data... From 5130613d1f0d8267cd067b76e0a4d6d68964870f Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Mon, 22 Apr 2024 09:04:47 +0200 Subject: [PATCH 4/6] Fix invalid text --- src/create/create.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/create/create.nim b/src/create/create.nim index c9b01f7d..f572af72 100644 --- a/src/create/create.nim +++ b/src/create/create.nim @@ -41,7 +41,7 @@ proc create*(conf: Conf) = stderr.writeLine msg quit QuitFailure if conf.action.difficulty.isSome: - let msg = "The difficulty argument is not supported for approaches" + let msg = "The difficulty argument is not supported for articles" stderr.writeLine msg quit QuitFailure let trackConfigPath = conf.trackDir / "config.json" From 80f9c804868fcf9baeb4f1b5ede4cb1c286d7710 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Mon, 22 Apr 2024 09:05:57 +0200 Subject: [PATCH 5/6] Fixc whitespace --- src/create/exercises.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/create/exercises.nim b/src/create/exercises.nim index 9988d072..b78fccf7 100644 --- a/src/create/exercises.nim +++ b/src/create/exercises.nim @@ -1,5 +1,5 @@ import std/[sets, options, os, strformat] -import ".."/[cli, helpers, logger, fmt/exercises, fmt/track_config, +import ".."/[cli, helpers, logger, fmt/exercises, fmt/track_config, sync/probspecs, sync/sync, sync/sync_common, sync/sync_filepaths, sync/sync_metadata, types_exercise_config, types_track_config, uuid/uuid] @@ -56,7 +56,7 @@ proc syncExercise(conf: Conf, slug: Slug,) = proc setAuthor(conf: Conf, slug: Slug, trackDir: string, exerciseKind: ExerciseKind) = let configPath = trackDir / "exercises" / $exerciseKind / $slug / ".meta" / "config.json" var exerciseConfig = ExerciseConfig.init(exerciseKind, configPath) - let formattedConfig = + let formattedConfig = case exerciseKind of ekConcept: exerciseConfig.c.authors.add conf.action.author From 4c4a960a24183fd21515c539e247bb11edf7ba4c Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Wed, 1 May 2024 11:44:27 +0200 Subject: [PATCH 6/6] Add binary tests --- tests/test_binary_create.nim | 74 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/tests/test_binary_create.nim b/tests/test_binary_create.nim index 8f81ebab..c717486c 100644 --- a/tests/test_binary_create.nim +++ b/tests/test_binary_create.nim @@ -1,4 +1,4 @@ -import std/[os, osproc, strformat, strutils, unittest] +import std/[os, osproc, re, strformat, strutils, unittest] import exec import "."/[binary_helpers] @@ -133,5 +133,77 @@ proc main = """.unindent() testStatusThenReset(trackDir, expectedStatus) + test "create practice exercise with difficulty (creates the exercise files, and exits with 0)": + const expectedOutput = fmt""" + Updating cached 'problem-specifications' data... + Created practice exercise 'foo'. + """.unindent() + execAndCheck(0, &"{createBase} --practice-exercise=foo --difficulty=5", expectedOutput) + + const expectedDiff = """ + --- config.json + +++ config.json + + }, + + { + + "slug": "foo", + + "name": "foo", + + "uuid": "", + + "practices": [], + + "prerequisites": [], + + "difficulty": 5 + """.unindent().replace("\p", "\n") + let diff = gitDiffConcise(trackDir).replace(re""""uuid": "[^"]+"""", """"uuid": """"") + check diff == expectedDiff + + const expectedStatus = """ + M config.json + A exercises/practice/foo/.docs/instructions.md + A exercises/practice/foo/.meta/config.json + A exercises/practice/foo/.meta/example.ex + A exercises/practice/foo/lib/foo.ex + A exercises/practice/foo/test/foo_test.exs + """.unindent() + testStatusThenReset(trackDir, expectedStatus) + + test "create practice exercise with author (creates the exercise files, and exits with 0)": + const expectedOutput = fmt""" + Updating cached 'problem-specifications' data... + Created practice exercise 'foo'. + """.unindent() + execAndCheck(0, &"{createBase} --practice-exercise=foo --author=bar", expectedOutput) + + const expectedConfig = """ + { + "authors": [ + "bar" + ], + "files": { + "solution": [ + "lib/foo.ex" + ], + "test": [ + "test/foo_test.exs" + ], + "example": [ + ".meta/example.ex" + ] + }, + "blurb": "" + } + """.dedent(6).replace("\p", "\n") + let configPath = trackDir / "exercises" / "practice" / "foo" / ".meta" / "config.json" + let config = readFile(configPath) + check config == expectedConfig + + const expectedStatus = """ + M config.json + A exercises/practice/foo/.docs/instructions.md + A exercises/practice/foo/.meta/config.json + A exercises/practice/foo/.meta/example.ex + A exercises/practice/foo/lib/foo.ex + A exercises/practice/foo/test/foo_test.exs + """.unindent() + testStatusThenReset(trackDir, expectedStatus) + main() {.used.}