Skip to content

Commit

Permalink
Add gomod-sync tool (#1954)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrerfcsantos authored Jan 13, 2022
1 parent e1e742a commit c92f37e
Show file tree
Hide file tree
Showing 26 changed files with 1,589 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/gomod-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: go.mod check

on:
workflow_dispatch:
push:
branches:
- main
pull_request:

jobs:
check:
name: go.mod check
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- uses: actions/setup-go@v2
with:
go-version: 1.17

- name: Check go.mod files
shell: bash
run: |
cd gomod-sync
go run main.go check
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ bin/configlet
bin/configlet.exe
bin/golangci-lint
bin/golangci-lint.exe

# gomod-sync

gomod-sync/gomod-sync
gomod-sync/gomod-sync.exe
gomod-sync/vendor
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,23 @@ To regenerate the test cases, navigate into the **go** directory and run
`GO111MODULE=off go run exercises/practice/<exercise>/.meta/gen.go`. You should see that the
`<exercise>/cases_test.go` file has changed. Commit the change.

## Managing the Go version

For an easy managment of the Go version in the `go.mod` file in all exercises, we can use `gomod-sync`.
This is a tool made in Go that can be seen in the `gomod-sync/` folder.

To update all go.mod files according to the config file (`gomod-sync/config.json`) run:

```console
$ cd gomod-sync && go run main.go update
```

To check all exercise go.mod files specify the correct Go version, run:

```console
$ cd gomod-sync && go run main.go check
```

## Pull requests

Pull requests are welcome.
Expand Down
152 changes: 152 additions & 0 deletions gomod-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# gomod-sync

Utility tool to check and update the Go version specified in the `go.mod` files of all exercises.
It works by specifying the desired Go version for all the `go.mod` files to be in. The `check` command
will verify if all `go.mod` files are in the desired version and the `update` command will update all
`go.mod` files to have the desired Go version.

Some exercises must have its `go.mod` specify a Go version that is different from the other exercise's `go.mod`.
This is supported by the `exceptions` key of the configuration file, where an entry must exist for each exercise
that must not have the default version.

## Quick start

To update all go.mod files according to the config file (gomod-sync/config.json) run:

```console
$ cd gomod-sync
$ go run main.go update
```

To check all exercise go.mod files specify the correct Go version, run:

```console
$ cd gomod-sync
$ go run main.go check
```

## Installing

### Compiling locally

```console
$ cd gomod-sync
$ go build
```

This will create an executable `gomod-sync` (`gomod-sync.exe` in windows) in the current directory
that you can run to execute the program.

### Running without compiling

```console
$ cd gomod-sync
$ go run main.go <command> [flags]
```

### Running the tests

```console
$ cd gomod-sync
$ go test ./...
```

## Usage

```
gomod-sync commandUpdate gitig [flags]
Available Commands:
check Checks if all go.mod files are in the target version
update Updates go.mod files to the target version
help Help about any command
```

## Commands

- `gomod-sync check -v target_version [-e exercises_path] [-c config_file]`

checks if all `go.mod` files are in the target version

- `gomod-sync completion`

generate the autocompletion script for the specified shell
- `gomod-sync help`

Help about any command
- `gomod-sync list [-e exercises_path]`

list `go.mod` files and the Go version they specify
- `gomod-sync update -v target_version [-e exercises_path] [-c config_file]`

updates `go.mod` files to the target version

## Flags

- `-c, --config config_file`

path to the JSON configuration file. (default `"config.json"`)

- `-e, --exercises exercises_path`

path to the exercises folder. `go.mod` files will be recursively searched inside this directory. (default `"../exercises"`)
- `-v, --goversion target_version`

target go version that all go.mod files are expected to have.
This will be used to check if the `go.mod` files are in the expected version in case of the check command,
and to update all `go.mod` files to this version in the case of the update command.
Using this flag will override the version specified in the config file.

- `-h, --help`

help for gomod-sync


## Configuration file

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.
This file can be given to the program with the `-c, --config file` flag. If the flag is omitted, a file `config.json`
in the current directory will be tried.

With a configuration file, in addition to define a default Go version all exercises' `go.mod` must have,
it's also possible to configure different versions for different exercises. This can be useful if a particular exercise
needs a superior version of Go than the default.

This an example of such configuration file:

```json
{
"default": "1.16",
"exceptions": [
{
"exercise": "strain",
"version": "1.18"
}
]
}
```

With such configuration, all `go.mod` files will be expected to have the `1.16` version of Go,
except the exercise `strain`, which must have version `1.18` in its `go.mod`.
Specifying the `-v, --goversion` flag overrides the default version specified in this file.

## Examples


* Check if all `go.mod` files of exercises in the `../exercises` folder have the default version
specified in the `config.json` file:

* `gomod-sync check`

* Check if all `go.mod` files of exercises in the `exercises` folder have the `1.16` Go version:

* `gomod-sync check --goversion 1.16 --exercises ./exercises`

* Update all `go.mod` files of exercises in the `exercises` folder have the `1.16` Go version:

* `gomod-sync update --goversion 1.16 --exercises ./exercises`

* Update all `go.mod` files, using a config file to specify the versions of exercises:

* `gomod-sync update --config a_dir/config.json --exercises ./exercises`
52 changes: 52 additions & 0 deletions gomod-sync/cmd/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cmd

import (
"fmt"

"github.com/exercism/go/gomod-sync/gomod"
"github.com/logrusorgru/aurora/v3"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(checkCmd)
}

var checkCmd = &cobra.Command{
SilenceErrors: true,
Use: "check",
Short: "Checks if all go.mod files are in the target version",
PersistentPreRunE: loadConfig,
RunE: func(cmd *cobra.Command, args []string) error {
files, err := gomod.Infos(exercisesPathFlag)
if err != nil {
return err
}

type faultyFile struct {
gomod.Info
ExpectedVersion string
}

var faultyFiles []faultyFile
for _, file := range files {
expectedVersion := versionConfig.ExerciseExpectedVersion(file.ExerciseSlug)
if file.GoVersion != expectedVersion {
fmt.Println(aurora.Red(fmt.Sprintf("%v has version %s, but %s expected - FAIL", file.Path, file.GoVersion, expectedVersion)))
faultyFiles = append(faultyFiles, faultyFile{Info: file, ExpectedVersion: expectedVersion})
} else {
fmt.Println(aurora.Green(fmt.Sprintf("%v has version %s as expected - OK", file.Path, file.GoVersion)))
}
}

if len(faultyFiles) > 0 {
fmt.Println(aurora.Red(fmt.Sprintf("The following %d go.mod file(s) do not have the correct version set:", len(faultyFiles))))
for _, file := range faultyFiles {
fmt.Println(aurora.Red(fmt.Sprintf("\t%v has version %s, but %s expected", file.Path, file.GoVersion, file.ExpectedVersion)))
}
return fmt.Errorf("%d go.mod file(s) are not in the target version", len(faultyFiles))
}

return nil
},
}
25 changes: 25 additions & 0 deletions gomod-sync/cmd/config/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package config

import (
"encoding/json"
"fmt"
"os"
)

// Load loads a configuration from a path to a JSON file.
func Load(file string) (VersionConfig, error) {
var config VersionConfig

f, err := os.Open(file)
if err != nil {
return config, fmt.Errorf("failed to open config file: %v", err)
}
defer f.Close()

err = json.NewDecoder(f).Decode(&config)
if err != nil {
return config, fmt.Errorf("failed to decode config file: %v", err)
}

return config, nil
}
103 changes: 103 additions & 0 deletions gomod-sync/cmd/config/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package config_test

import (
"path/filepath"
"testing"

"github.com/exercism/go/gomod-sync/cmd/config"
)

func TestLoad(t *testing.T) {
tests := []struct {
Name string
Path string
Expected config.VersionConfig
ExpectError bool
}{
{
Name: "Loading non-existent config",
Path: filepath.Join("..", "..", "testdata", "non_existent.json"),
Expected: config.VersionConfig{},
ExpectError: true,
},
{
Name: "Loading config with no exceptions",
Path: filepath.Join("..", "..", "testdata", "version_config_no_exceptions.json"),
Expected: config.VersionConfig{
Default: "1.16",
},
ExpectError: false,
},
{
Name: "Loading config with 1 exception",
Path: filepath.Join("..", "..", "testdata", "version_config_one_exception.json"),
Expected: config.VersionConfig{
Default: "1.16",
Exceptions: []config.ExerciseVersion{
{
Exercise: "exercise01",
Version: "1.17",
},
},
},
ExpectError: false,
},
{
Name: "Loading config with 2 exceptions",
Path: filepath.Join("..", "..", "testdata", "version_config_two_exceptions.json"),
Expected: config.VersionConfig{
Default: "1.16",
Exceptions: []config.ExerciseVersion{
{
Exercise: "exercise01",
Version: "1.17",
},
{
Exercise: "exercise02",
Version: "1.17",
},
},
},
ExpectError: false,
},
}

for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
config, err := config.Load(test.Path)

if test.ExpectError && err == nil {
t.Fatalf("expected error, but got none")
}

if !test.ExpectError && err != nil {
t.Fatalf("didn't expect error, but got %v", err)
}

if !configEqual(config, test.Expected) {
t.Fatalf("expected config %+v, but got %+v", test.Expected, config)
}
})
}
}

func configEqual(a, b config.VersionConfig) bool {
return a.Default == b.Default && equalExceptions(a.Exceptions, b.Exceptions)
}

// equalExceptions compares two lists of exercise versions and tells if they are equal.
// Two exercise list versions are considered equal if they contain the same elements
// in the same order
func equalExceptions(a, b []config.ExerciseVersion) bool {
if len(a) != len(b) {
return false
}

for i := range a {
if a[i] != b[i] {
return false
}
}

return true
}
Loading

0 comments on commit c92f37e

Please sign in to comment.