Skip to content

Commit

Permalink
Add gomod-sync
Browse files Browse the repository at this point in the history
  • Loading branch information
andrerfcsantos committed Jan 2, 2022
1 parent e533151 commit 70a51d8
Show file tree
Hide file tree
Showing 27 changed files with 1,640 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
15 changes: 15 additions & 0 deletions gomod-sync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/
138 changes: 138 additions & 0 deletions gomod-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# 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.

## 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 command [flags]
Available Commands:
check Checks if all go.mod files are in the target version
completion generate the autocompletion script for the specified shell
help Help about any command
list List go.mod files and the Go version they specify
update Updates go.mod files to the target version
```

## 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
}
82 changes: 82 additions & 0 deletions gomod-sync/cmd/config/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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 non-existent config",
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: "exercise02",
Version: "1.17",
},
{
Exercise: "exercise01",
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 !config.Equal(test.Expected) {
t.Fatalf("expected config %+v, but got %+v", test.Expected, config)
}
})
}
}
Loading

0 comments on commit 70a51d8

Please sign in to comment.