Skip to content

Commit c92f37e

Browse files
Add gomod-sync tool (exercism#1954)
1 parent e1e742a commit c92f37e

26 files changed

+1589
-0
lines changed

.github/workflows/gomod-sync.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: go.mod check
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
pull_request:
9+
10+
jobs:
11+
check:
12+
name: go.mod check
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v2
17+
18+
- uses: actions/setup-go@v2
19+
with:
20+
go-version: 1.17
21+
22+
- name: Check go.mod files
23+
shell: bash
24+
run: |
25+
cd gomod-sync
26+
go run main.go check

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ bin/configlet
66
bin/configlet.exe
77
bin/golangci-lint
88
bin/golangci-lint.exe
9+
10+
# gomod-sync
11+
12+
gomod-sync/gomod-sync
13+
gomod-sync/gomod-sync.exe
14+
gomod-sync/vendor

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,23 @@ To regenerate the test cases, navigate into the **go** directory and run
225225
`GO111MODULE=off go run exercises/practice/<exercise>/.meta/gen.go`. You should see that the
226226
`<exercise>/cases_test.go` file has changed. Commit the change.
227227

228+
## Managing the Go version
229+
230+
For an easy managment of the Go version in the `go.mod` file in all exercises, we can use `gomod-sync`.
231+
This is a tool made in Go that can be seen in the `gomod-sync/` folder.
232+
233+
To update all go.mod files according to the config file (`gomod-sync/config.json`) run:
234+
235+
```console
236+
$ cd gomod-sync && go run main.go update
237+
```
238+
239+
To check all exercise go.mod files specify the correct Go version, run:
240+
241+
```console
242+
$ cd gomod-sync && go run main.go check
243+
```
244+
228245
## Pull requests
229246

230247
Pull requests are welcome.

gomod-sync/README.md

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# gomod-sync
2+
3+
Utility tool to check and update the Go version specified in the `go.mod` files of all exercises.
4+
It works by specifying the desired Go version for all the `go.mod` files to be in. The `check` command
5+
will verify if all `go.mod` files are in the desired version and the `update` command will update all
6+
`go.mod` files to have the desired Go version.
7+
8+
Some exercises must have its `go.mod` specify a Go version that is different from the other exercise's `go.mod`.
9+
This is supported by the `exceptions` key of the configuration file, where an entry must exist for each exercise
10+
that must not have the default version.
11+
12+
## Quick start
13+
14+
To update all go.mod files according to the config file (gomod-sync/config.json) run:
15+
16+
```console
17+
$ cd gomod-sync
18+
$ go run main.go update
19+
```
20+
21+
To check all exercise go.mod files specify the correct Go version, run:
22+
23+
```console
24+
$ cd gomod-sync
25+
$ go run main.go check
26+
```
27+
28+
## Installing
29+
30+
### Compiling locally
31+
32+
```console
33+
$ cd gomod-sync
34+
$ go build
35+
```
36+
37+
This will create an executable `gomod-sync` (`gomod-sync.exe` in windows) in the current directory
38+
that you can run to execute the program.
39+
40+
### Running without compiling
41+
42+
```console
43+
$ cd gomod-sync
44+
$ go run main.go <command> [flags]
45+
```
46+
47+
### Running the tests
48+
49+
```console
50+
$ cd gomod-sync
51+
$ go test ./...
52+
```
53+
54+
## Usage
55+
56+
```
57+
gomod-sync commandUpdate gitig [flags]
58+
59+
Available Commands:
60+
check Checks if all go.mod files are in the target version
61+
update Updates go.mod files to the target version
62+
help Help about any command
63+
64+
```
65+
66+
## Commands
67+
68+
- `gomod-sync check -v target_version [-e exercises_path] [-c config_file]`
69+
70+
checks if all `go.mod` files are in the target version
71+
72+
- `gomod-sync completion`
73+
74+
generate the autocompletion script for the specified shell
75+
- `gomod-sync help`
76+
77+
Help about any command
78+
- `gomod-sync list [-e exercises_path]`
79+
80+
list `go.mod` files and the Go version they specify
81+
- `gomod-sync update -v target_version [-e exercises_path] [-c config_file]`
82+
83+
updates `go.mod` files to the target version
84+
85+
## Flags
86+
87+
- `-c, --config config_file`
88+
89+
path to the JSON configuration file. (default `"config.json"`)
90+
91+
- `-e, --exercises exercises_path`
92+
93+
path to the exercises folder. `go.mod` files will be recursively searched inside this directory. (default `"../exercises"`)
94+
- `-v, --goversion target_version`
95+
96+
target go version that all go.mod files are expected to have.
97+
This will be used to check if the `go.mod` files are in the expected version in case of the check command,
98+
and to update all `go.mod` files to this version in the case of the update command.
99+
Using this flag will override the version specified in the config file.
100+
101+
- `-h, --help`
102+
103+
help for gomod-sync
104+
105+
106+
## Configuration file
107+
108+
Besides the `-v, --goversion` flag, it is also possible to specify the expected go versions for the `go.mod` files in a JSON configuration file.
109+
This file can be given to the program with the `-c, --config file` flag. If the flag is omitted, a file `config.json`
110+
in the current directory will be tried.
111+
112+
With a configuration file, in addition to define a default Go version all exercises' `go.mod` must have,
113+
it's also possible to configure different versions for different exercises. This can be useful if a particular exercise
114+
needs a superior version of Go than the default.
115+
116+
This an example of such configuration file:
117+
118+
```json
119+
{
120+
"default": "1.16",
121+
"exceptions": [
122+
{
123+
"exercise": "strain",
124+
"version": "1.18"
125+
}
126+
]
127+
}
128+
```
129+
130+
With such configuration, all `go.mod` files will be expected to have the `1.16` version of Go,
131+
except the exercise `strain`, which must have version `1.18` in its `go.mod`.
132+
Specifying the `-v, --goversion` flag overrides the default version specified in this file.
133+
134+
## Examples
135+
136+
137+
* Check if all `go.mod` files of exercises in the `../exercises` folder have the default version
138+
specified in the `config.json` file:
139+
140+
* `gomod-sync check`
141+
142+
* Check if all `go.mod` files of exercises in the `exercises` folder have the `1.16` Go version:
143+
144+
* `gomod-sync check --goversion 1.16 --exercises ./exercises`
145+
146+
* Update all `go.mod` files of exercises in the `exercises` folder have the `1.16` Go version:
147+
148+
* `gomod-sync update --goversion 1.16 --exercises ./exercises`
149+
150+
* Update all `go.mod` files, using a config file to specify the versions of exercises:
151+
152+
* `gomod-sync update --config a_dir/config.json --exercises ./exercises`

gomod-sync/cmd/check.go

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/exercism/go/gomod-sync/gomod"
7+
"github.com/logrusorgru/aurora/v3"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func init() {
12+
rootCmd.AddCommand(checkCmd)
13+
}
14+
15+
var checkCmd = &cobra.Command{
16+
SilenceErrors: true,
17+
Use: "check",
18+
Short: "Checks if all go.mod files are in the target version",
19+
PersistentPreRunE: loadConfig,
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
files, err := gomod.Infos(exercisesPathFlag)
22+
if err != nil {
23+
return err
24+
}
25+
26+
type faultyFile struct {
27+
gomod.Info
28+
ExpectedVersion string
29+
}
30+
31+
var faultyFiles []faultyFile
32+
for _, file := range files {
33+
expectedVersion := versionConfig.ExerciseExpectedVersion(file.ExerciseSlug)
34+
if file.GoVersion != expectedVersion {
35+
fmt.Println(aurora.Red(fmt.Sprintf("%v has version %s, but %s expected - FAIL", file.Path, file.GoVersion, expectedVersion)))
36+
faultyFiles = append(faultyFiles, faultyFile{Info: file, ExpectedVersion: expectedVersion})
37+
} else {
38+
fmt.Println(aurora.Green(fmt.Sprintf("%v has version %s as expected - OK", file.Path, file.GoVersion)))
39+
}
40+
}
41+
42+
if len(faultyFiles) > 0 {
43+
fmt.Println(aurora.Red(fmt.Sprintf("The following %d go.mod file(s) do not have the correct version set:", len(faultyFiles))))
44+
for _, file := range faultyFiles {
45+
fmt.Println(aurora.Red(fmt.Sprintf("\t%v has version %s, but %s expected", file.Path, file.GoVersion, file.ExpectedVersion)))
46+
}
47+
return fmt.Errorf("%d go.mod file(s) are not in the target version", len(faultyFiles))
48+
}
49+
50+
return nil
51+
},
52+
}

gomod-sync/cmd/config/load.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// Load loads a configuration from a path to a JSON file.
10+
func Load(file string) (VersionConfig, error) {
11+
var config VersionConfig
12+
13+
f, err := os.Open(file)
14+
if err != nil {
15+
return config, fmt.Errorf("failed to open config file: %v", err)
16+
}
17+
defer f.Close()
18+
19+
err = json.NewDecoder(f).Decode(&config)
20+
if err != nil {
21+
return config, fmt.Errorf("failed to decode config file: %v", err)
22+
}
23+
24+
return config, nil
25+
}

gomod-sync/cmd/config/load_test.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package config_test
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/exercism/go/gomod-sync/cmd/config"
8+
)
9+
10+
func TestLoad(t *testing.T) {
11+
tests := []struct {
12+
Name string
13+
Path string
14+
Expected config.VersionConfig
15+
ExpectError bool
16+
}{
17+
{
18+
Name: "Loading non-existent config",
19+
Path: filepath.Join("..", "..", "testdata", "non_existent.json"),
20+
Expected: config.VersionConfig{},
21+
ExpectError: true,
22+
},
23+
{
24+
Name: "Loading config with no exceptions",
25+
Path: filepath.Join("..", "..", "testdata", "version_config_no_exceptions.json"),
26+
Expected: config.VersionConfig{
27+
Default: "1.16",
28+
},
29+
ExpectError: false,
30+
},
31+
{
32+
Name: "Loading config with 1 exception",
33+
Path: filepath.Join("..", "..", "testdata", "version_config_one_exception.json"),
34+
Expected: config.VersionConfig{
35+
Default: "1.16",
36+
Exceptions: []config.ExerciseVersion{
37+
{
38+
Exercise: "exercise01",
39+
Version: "1.17",
40+
},
41+
},
42+
},
43+
ExpectError: false,
44+
},
45+
{
46+
Name: "Loading config with 2 exceptions",
47+
Path: filepath.Join("..", "..", "testdata", "version_config_two_exceptions.json"),
48+
Expected: config.VersionConfig{
49+
Default: "1.16",
50+
Exceptions: []config.ExerciseVersion{
51+
{
52+
Exercise: "exercise01",
53+
Version: "1.17",
54+
},
55+
{
56+
Exercise: "exercise02",
57+
Version: "1.17",
58+
},
59+
},
60+
},
61+
ExpectError: false,
62+
},
63+
}
64+
65+
for _, test := range tests {
66+
t.Run(test.Name, func(t *testing.T) {
67+
config, err := config.Load(test.Path)
68+
69+
if test.ExpectError && err == nil {
70+
t.Fatalf("expected error, but got none")
71+
}
72+
73+
if !test.ExpectError && err != nil {
74+
t.Fatalf("didn't expect error, but got %v", err)
75+
}
76+
77+
if !configEqual(config, test.Expected) {
78+
t.Fatalf("expected config %+v, but got %+v", test.Expected, config)
79+
}
80+
})
81+
}
82+
}
83+
84+
func configEqual(a, b config.VersionConfig) bool {
85+
return a.Default == b.Default && equalExceptions(a.Exceptions, b.Exceptions)
86+
}
87+
88+
// equalExceptions compares two lists of exercise versions and tells if they are equal.
89+
// Two exercise list versions are considered equal if they contain the same elements
90+
// in the same order
91+
func equalExceptions(a, b []config.ExerciseVersion) bool {
92+
if len(a) != len(b) {
93+
return false
94+
}
95+
96+
for i := range a {
97+
if a[i] != b[i] {
98+
return false
99+
}
100+
}
101+
102+
return true
103+
}

0 commit comments

Comments
 (0)