Skip to content

Commit 861ea64

Browse files
Simplify add new exercise script (exercism#721)
scripts: document and simplify
1 parent 6138c27 commit 861ea64

11 files changed

+333
-185
lines changed
Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
There is a new exercise, [EXERCISE-NAME](https://github.com/exercism/problem-specifications/blob/master/exercises/EXERCISE-NAME/description.md), which data can be found here: https://github.com/exercism/problem-specifications/tree/master/exercises/EXERCISE-NAME
22

3-
To implement the `EXERCISE-NAME` exercise, the following needs to be done:
4-
5-
* Create a directory for the new exercise in the `exercises` directory (note: this has to exactly match `EXERCISE-NAME`).
6-
* Add a new entry to the [config.json](https://github.com/exercism/fsharp/blob/master/config.json) file.
7-
* Create a generator to automatically convert the [canonical data](https://github.com/exercism/problem-specifications/blob/master/exercises/EXERCISE-NAME/canonical-data.json) to a test file. For more information on how to do this, check the [generators docs](https://github.com/exercism/fsharp/blob/master/docs/GENERATORS.md).
8-
* Create a project file for the project (you can probably best copy and modify one of the existing projects).
9-
* Create a stub implementation file that contains the code necessary to compile (which are the methods called by the test file), but lacks an implementation. (see [this example](https://github.com/exercism/fsharp/blob/master/exercises/two-fer/TwoFer.fs)).
10-
* Create an implementation file that passes all the tests. You can verify this by running `.\build.ps1 --exercise=EXERCISE-NAME` from the root directory.
3+
To implement the `EXERCISE-NAME` exercise, first run the `./add-new-exercise EXERCISE-NAME` script that will create and update the files required for the new exercise. After this script has run, it will have done the following:
4+
5+
- Added a new entry for the exercise to the [config.json](https://github.com/exercism/fsharp/blob/master/config.json) file.
6+
- Add a default generator to the [generator/Generators.fs] file, which is used to automatically convert the [canonical data](https://github.com/exercism/problem-specifications/blob/master/exercises/EXERCISE-NAME/canonical-data.json) to a test file. For more information on how this works, check the [generators docs](https://github.com/exercism/fsharp/blob/master/docs/GENERATORS.md).
7+
- Created the [`exercises/EXERCISE-NAME`] directory, which contains the following exercise-specific files:
8+
9+
- A project file.
10+
- A test file.
11+
- A stub implementation file.
12+
- An example implementation file.
13+
- A `README.md` file describing the exercise.
14+
15+
Once these files have been created, what's left for you is to:
16+
17+
- Verify the tests in the test file. As the test suite is automatically generated based on the canonical data, any customizations should be done by updating the generator in the [generator/Generators.fs] file and then regenerating the test suite using `./generate-tests.ps1 EXERCISE-NAME`.
18+
- Modify the stub implementation file to have it compile succesfully together with the test file. This means adding stubs for the functions required in the test suite. For an example, see the [two-fer stub implementation file](https://github.com/exercism/fsharp/blob/master/exercises/two-fer/TwoFer.fs))
19+
- Modify the example implementation file to have it pass all the tests. You can verify this by running `./test.ps1 EXERCISE-NAME` from the root directory.
20+
- Update the exercise's entry in the `config.json` file: adding topics, setting the difficulty and indicating if it is a core exercise, or else specifying which exercise unlocks it.
21+
22+
Once all these steps have been completed, the final step is to open a pull request :)

.travis.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ dotnet: 2.2.203
55
addons:
66
apt:
77
packages:
8-
- powershell
8+
- powershell
99
env:
1010
global:
1111
- DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
1212
- DOTNET_CLI_TELEMETRY_OPTOUT: 1
1313
before_install:
1414
- export PATH=$HOME/.dotnet/tools:$PATH
1515
script:
16-
- pwsh ./build.ps1
16+
- pwsh ./test.ps1
1717
cache:
1818
directories:
19-
- $HOME/.nuget/packages
20-
- $HOME/.dotnet/tools
19+
- $HOME/.nuget/packages
20+
- $HOME/.dotnet/tools

add-new-exercise.ps1

Lines changed: 99 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,108 @@
1+
<#
2+
.SYNOPSIS
3+
Add a new exercise.
4+
.DESCRIPTION
5+
Add the files need to add a new exercise.
6+
.PARAMETER Exercise
7+
The slug of the exercise to add.
8+
.PARAMETER Topics
9+
The topics of the exercise (optional).
10+
.PARAMETER Core
11+
Indicates if the exercise is a core exercise (optional).
12+
.PARAMETER Difficulty
13+
The difficulty of the exercise on a scale from 1 to 10 (optional).
14+
.PARAMETER UnlockedBy
15+
The slug of the exercise that unlocks exercise (optional).
16+
.EXAMPLE
17+
The example below will add the "acronym" exercise
18+
PS C:\> ./add-new-exercise.ps1 acronym
19+
.EXAMPLE
20+
The example below will add the "acronym" exercise as a core exercise
21+
PS C:\> ./add-new-exercise.ps1 acronym -Core
22+
.EXAMPLE
23+
The example below will add the "acronym" exercise as a core exercise, with
24+
two topics, a specified difficulty and being unlocked by "two-fer"
25+
PS C:\> ./add-new-exercise.ps1 acronym -Core -Topics strings,optional -Difficulty 3 -UnlockedBy two-fer
26+
#>
27+
128
param (
2-
[Parameter(Mandatory = $true)][string]$Exercise,
3-
[Parameter()][string[]]$Topics = $null,
4-
[Parameter()][bool]$Core,
5-
[Parameter()][int32]$Difficulty = 1,
6-
[Parameter()][String]$UnlockedBy = $null
29+
[Parameter(Position = 0, Mandatory = $true)][string]$Exercise,
30+
[Parameter()][string[]]$Topics = @(),
31+
[Parameter()][switch]$Core,
32+
[Parameter()][int]$Difficulty = 1,
33+
[Parameter()]$UnlockedBy
734
)
835

9-
class Exercise {
10-
[String]$slug = ""
11-
[guid]$uuid = [Guid]::Empty
12-
[Boolean]$core = $false
13-
[string]$unlocked_by = $null
14-
[int32]$difficulty = 1
15-
[string[]]$topics = $null
16-
17-
Exercise ([String]$Slug, [String[]]$Topics, [Boolean]$Core, [int32]$Difficulty, [String]$UnlockedBy) {
18-
$this.slug = $Slug
19-
$this.topics = $Topics
20-
$this.uuid = [Guid]::NewGuid()
21-
$this.core = $Core
22-
$this.difficulty = $Difficulty
23-
$this.unlocked_by = $UnlockedBy
24-
}
36+
# Import shared functionality
37+
. ./shared.ps1
38+
39+
$exerciseName = (Get-Culture).TextInfo.ToTitleCase($Exercise).Replace("-", "")
40+
$exercisesDir = Resolve-Path "exercises"
41+
$exerciseDir = Join-Path $exercisesDir $Exercise
42+
43+
function Add-Project {
44+
Write-Output "Adding project"
45+
46+
$fsProj = "$exerciseDir/$exerciseName.fsproj"
47+
48+
Run-Command "dotnet new xunit -lang ""F#"" -o $exerciseDir -n $exerciseName"
49+
Run-Command "dotnet sln ""$exercisesDir/Exercises.sln"" add $fsProj"
50+
51+
Remove-Item -Path "$exerciseDir/Program.fs"
52+
Remove-Item -Path "$exerciseDir/Tests.fs"
53+
54+
New-Item -ItemType File -Path "$exerciseDir/$exerciseName.fs" -Value "module $exerciseName"
55+
New-Item -ItemType File -Path "$exerciseDir/Example.fs" -Value "module $exerciseName"
56+
57+
[xml]$proj = Get-Content $fsProj
58+
$proj.Project.ItemGroup[0].Compile[0].Include = "$exerciseName.fs"
59+
$proj.Project.ItemGroup[0].Compile[1].Include = "${exerciseName}Test.fs"
60+
$proj.Save($fsProj)
2561
}
2662

27-
function Restore-Indentation {
28-
$newContent = $args[0]
29-
$newContent = $newContent -replace " {41}", " "
30-
$newContent = $newContent -replace " {37}", " "
31-
$newContent = $newContent -replace " {26}", " "
32-
$newContent = $newContent -replace " {22}", " "
33-
$newContent = $newContent -replace " {21}", " "
34-
$newContent = $newContent -replace " {17}", " "
35-
$newContent = $newContent -replace "(?<! |\n) {2}(?! )", " "
36-
$newContent
63+
function Add-Generator {
64+
Write-Output "Adding generator"
65+
66+
$generatorsDir = Resolve-Path "generators"
67+
$generator = "type $exerciseName() =`n inherit GeneratorExercise()`n"
68+
Add-Content -Path "$generatorsDir/Generators.fs" -Value $generator
69+
}
70+
71+
function Update-Readme {
72+
Write-Output "Updating README"
73+
./update-docs.ps1 $Exercise
74+
}
75+
76+
function Update-Tests {
77+
Write-Output "Updating test suite"
78+
./generate-tests.ps1 $Exercise
79+
}
80+
81+
function Update-Config-Json {
82+
Write-Output "Updating config.json"
83+
84+
$configJson = Resolve-Path "config.json"
85+
86+
$config = Get-Content $configJson | ConvertFrom-JSON
87+
$config.exercises += [pscustomobject]@{
88+
slug = $Exercise;
89+
uuid = [Guid]::NewGuid();
90+
core = $Core.IsPresent;
91+
unlocked_by = $UnlockedBy;
92+
difficulty = $Difficulty;
93+
topics = $Topics;
94+
}
95+
96+
ConvertTo-Json -InputObject $config -Depth 10 | Set-Content -Path $configJson
97+
98+
Run-Command "./bin/fetch-configlet"
99+
Run-Command "./bin/configlet fmt ."
37100
}
38101

39-
$projectName = (Get-Culture).TextInfo.ToTitleCase($Exercise).Replace("-", "")
40-
New-Item -Path "Exercises/$Exercise" -ItemType Directory
41-
$config = ConvertFrom-JSON -InputObject ([IO.File]::ReadAllText("./config.json"))
42-
$Exercises = $config.Exercises
43-
$newExercise = New-Object -Typename Exercise -ArgumentList $Exercise, $Topics, $Core, $Difficulty, $UnlockedBy
44-
$Exercises += $newExercise
45-
$config.Exercises = $Exercises;
46-
$newContent = ConvertTo-Json -InputObject $config -Depth 10
47-
$newContent = Restore-Indentation $newContent
48-
$newContent = $newContent -replace "\\u0027", "'"
49-
$newContent = $newContent -replace 'unlocked_by": ""', 'unlocked_by": null'
50-
[IO.File]::WriteAllText("./config.json", $newContent)
51-
52-
$fsProj = "Exercises/$Exercise/$projectName.fsproj"
53-
dotnet new xunit -lang F# -o "Exercises/$Exercise" -n $projectName
54-
dotnet sln "Exercises/Exercises.sln" add $fsProj
55-
Remove-Item -Path "Exercises/$Exercise/Program.fs"
56-
Remove-Item -Path "Exercises/$Exercise/Tests.fs"
57-
58-
New-Item -Path "Exercises/$Exercise/$projectName.fs" -ItemType File
59-
New-Item -Path ("Exercises/$Exercise/${projectName}Test.fs") -ItemType File
60-
New-Item -Path "Exercises/$Exercise/Example.fs" -ItemType File
61-
[xml]$proj = Get-Content $fsProj
62-
63-
$proj.Project.ItemGroup[0].Compile[0].Include = "$projectName.fs"
64-
$proj.Project.ItemGroup[0].Compile[1].Include = "${projectName}Test.fs"
65-
$proj.Save($fsProj)
102+
Add-Project
103+
Add-Generator
104+
Update-Readme
105+
Update-Tests
106+
Update-Config-Json
66107

67108
exit $LastExitCode

build.cake

Lines changed: 0 additions & 104 deletions
This file was deleted.

build.ps1

Lines changed: 0 additions & 4 deletions
This file was deleted.

generate-tests.ps1

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<#
2+
.SYNOPSIS
3+
Generate tests.
4+
.DESCRIPTION
5+
Generate tests based on the latest canonical data.
6+
.PARAMETER Exercise
7+
The slug of the exercise to be analyzed (optional).
8+
.EXAMPLE
9+
The example below will regenerate all tests
10+
PS C:\> ./generate-tests.ps1
11+
.EXAMPLE
12+
The example below will regenerate the tests for the "acronym" exercise
13+
PS C:\> ./generate-tests.ps1 acronym
14+
#>
15+
16+
param (
17+
[Parameter(Position = 0, Mandatory = $false)]
18+
[string]$Exercise
19+
)
20+
21+
22+
# Import shared functionality
23+
. ./shared.ps1
24+
25+
function Update-Canonical-Data {
26+
Write-Output "Updating canonical data"
27+
Run-Command "./update-canonical-data.ps1"
28+
}
29+
30+
function Update-Tests {
31+
Write-Output "Updating tests"
32+
$args = if ($Exercise) { @("--exercise", $Exercise) } else { @() }
33+
Run-Command "dotnet run --project ./generators $args"
34+
}
35+
36+
Update-Canonical-Data
37+
Update-Tests

shared.ps1

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
$ErrorActionPreference = 'Stop'
2+
3+
# PowerShell does not check the return code of native commands.
4+
# There is a pending proposal to support this: https://github.com/PowerShell/PowerShell-RFC/pull/88/files
5+
function Run-Command ($Command) {
6+
<#
7+
.SYNOPSIS
8+
Run a native command.
9+
.PARAMETER Command
10+
The command to execute.
11+
.EXAMPLE
12+
The example below runs the "./bin/configlet hint ." command
13+
14+
Run-Command "./bin/configlet hint ."
15+
#>
16+
17+
Invoke-Expression $Command
18+
19+
if ($Lastexitcode -ne 0) {
20+
exit $Lastexitcode
21+
}
22+
}

0 commit comments

Comments
 (0)