refactor: generic language support

This commit is contained in:
Jan De Dobbeleer 2021-02-03 19:11:32 +01:00 committed by Jan De Dobbeleer
parent 7e813893ed
commit c43cbac284
7 changed files with 316 additions and 209 deletions

View file

@ -25,15 +25,17 @@ func (d *dotnet) string() string {
func (d *dotnet) init(props *properties, env environmentInfo) {
d.language = &language{
env: env,
props: props,
commands: []string{"dotnet"},
versionParam: "--version",
extensions: []string{"*.cs", "*.vb", "*.sln", "*.csproj", "*.vbproj"},
version: &version{
regex: `(?:(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?:\d{2})(?P<patch>[0-9]{1}))))`,
urlTemplate: "[%1s](https://github.com/dotnet/core/blob/master/release-notes/%[2]s.%[3]s/%[2]s.%[3]s.%[4]s/%[2]s.%[3]s.%[4]s.md)",
env: env,
props: props,
extensions: []string{"*.cs", "*.vb", "*.sln", "*.csproj", "*.vbproj"},
commands: []*cmd{
{
executable: "dotnet",
args: []string{"--version"},
regex: `(?:(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?:\d{2})(?P<patch>[0-9]{1}))))`,
},
},
versionURLTemplate: "[%1s](https://github.com/dotnet/core/blob/master/release-notes/%[2]s.%[3]s/%[2]s.%[3]s.%[4]s/%[2]s.%[3]s.%[4]s.md)",
}
}

View file

@ -10,15 +10,17 @@ func (g *golang) string() string {
func (g *golang) init(props *properties, env environmentInfo) {
g.language = &language{
env: env,
props: props,
commands: []string{"go"},
versionParam: "version",
extensions: []string{"*.go", "go.mod"},
version: &version{
regex: `(?:go(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
urlTemplate: "[%s](https://golang.org/doc/go%s.%s)",
env: env,
props: props,
extensions: []string{"*.go", "go.mod"},
commands: []*cmd{
{
executable: "go",
args: []string{"version"},
regex: `(?:go(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
},
},
versionURLTemplate: "[%s](https://golang.org/doc/go%s.%s)",
}
}

View file

@ -10,13 +10,15 @@ func (j *julia) string() string {
func (j *julia) init(props *properties, env environmentInfo) {
j.language = &language{
env: env,
props: props,
commands: []string{"julia"},
versionParam: "--version",
extensions: []string{"*.jl"},
version: &version{
regex: `julia version (?P<version>[0-9]+.[0-9]+.[0-9]+)`,
env: env,
props: props,
extensions: []string{"*.jl"},
commands: []*cmd{
{
executable: "julia",
args: []string{"--version"},
regex: `julia version (?P<version>[0-9]+.[0-9]+.[0-9]+)`,
},
},
}
}

View file

@ -11,38 +11,63 @@ type loadContext func()
type inContext func() bool
type version struct {
full string
major string
minor string
patch string
regex string
urlTemplate string
full string
major string
minor string
patch string
}
func (v *version) parse(versionInfo string) error {
values := findNamedRegexMatch(v.regex, versionInfo)
type cmd struct {
executable string
args []string
regex string
version *version
}
func (c *cmd) parse(versionInfo string) error {
values := findNamedRegexMatch(c.regex, versionInfo)
if len(values) == 0 {
return errors.New("cannot parse version string")
}
v.full = values["version"]
v.major = values["major"]
v.minor = values["minor"]
v.patch = values["patch"]
c.version = &version{}
c.version.full = values["version"]
c.version.major = values["major"]
c.version.minor = values["minor"]
c.version.patch = values["patch"]
return nil
}
func (c *cmd) buildVersionURL(template string) string {
if template == "" {
return c.version.full
}
truncatingSprintf := func(str string, args ...interface{}) (string, error) {
n := strings.Count(str, "%s")
if n > len(args) {
return "", errors.New("Too many parameters")
}
if n == 0 {
return fmt.Sprintf(str, args...), nil
}
return fmt.Sprintf(str, args[:n]...), nil
}
version, err := truncatingSprintf(template, c.version.full, c.version.major, c.version.minor, c.version.patch)
if err != nil {
return c.version.full
}
return version
}
type language struct {
props *properties
env environmentInfo
extensions []string
commands []string
executable string
versionParam string
version *version
exitCode int
loadContext loadContext
inContext inContext
props *properties
env environmentInfo
extensions []string
commands []*cmd
versionURLTemplate string
activeCommand *cmd
exitCode int
loadContext loadContext
inContext inContext
}
const (
@ -66,24 +91,16 @@ func (l *language) string() string {
if !l.props.getBool(DisplayVersion, true) {
return ""
}
if !l.hasCommand() {
return l.props.getString(MissingCommandTextProperty, MissingCommandText)
}
err := l.setVersion()
if err != nil {
return ""
return err.Error()
}
// build release notes hyperlink
if l.props.getBool(EnableHyperlink, false) && l.version.urlTemplate != "" {
version, err := TruncatingSprintf(l.version.urlTemplate, l.version.full, l.version.major, l.version.minor, l.version.patch)
if err != nil {
return l.version.full
}
return version
if l.props.getBool(EnableHyperlink, false) {
return l.activeCommand.buildVersionURL(l.versionURLTemplate)
}
return l.version.full
return l.activeCommand.version.full
}
func (l *language) enabled() bool {
@ -119,30 +136,23 @@ func (l *language) hasLanguageFiles() bool {
// setVersion parses the version string returned by the command
func (l *language) setVersion() error {
versionInfo, err := l.env.runCommand(l.executable, l.versionParam)
if exitErr, ok := err.(*commandError); ok {
l.exitCode = exitErr.exitCode
return errors.New("error executing command")
}
err = l.version.parse(versionInfo)
if err != nil {
return err
}
return nil
}
// hasCommand checks if one of the commands exists and sets it as executable
func (l *language) hasCommand() bool {
for i, command := range l.commands {
if l.env.hasCommand(command) {
l.executable = command
break
for _, command := range l.commands {
if !l.env.hasCommand(command.executable) {
continue
}
if i == len(l.commands)-1 {
return false
version, err := l.env.runCommand(command.executable, command.args...)
if exitErr, ok := err.(*commandError); ok {
l.exitCode = exitErr.exitCode
return fmt.Errorf("err executing %s with %s", command.executable, command.args)
}
err = command.parse(version)
if err != nil {
return fmt.Errorf("err parsing info from %s with %s", command.executable, version)
}
l.activeCommand = command
return nil
}
return true
return errors.New(l.props.getString(MissingCommandTextProperty, MissingCommandText))
}
func (l *language) loadLanguageContext() {
@ -158,14 +168,3 @@ func (l *language) inLanguageContext() bool {
}
return l.inContext()
}
func TruncatingSprintf(str string, args ...interface{}) (string, error) {
n := strings.Count(str, "%s")
if n > len(args) {
return "", errors.New("Too many parameters")
}
if n == 0 {
return fmt.Sprintf(str, args...), nil
}
return fmt.Sprintf(str, args[:n]...), nil
}

View file

@ -18,13 +18,12 @@ type languageArgs struct {
displayMode string
extensions []string
enabledExtensions []string
commands []string
commands []*cmd
enabledCommands []string
versionParam string
versionRegex string
missingCommandText string
urlTemplate string
versionURLTemplate string
enableHyperlink bool
expectedError error
}
func (l *languageArgs) hasvalue(value string, list []string) bool {
@ -39,8 +38,8 @@ func (l *languageArgs) hasvalue(value string, list []string) bool {
func bootStrapLanguageTest(args *languageArgs) *language {
env := new(MockedEnvironment)
for _, command := range args.commands {
env.On("hasCommand", command).Return(args.hasvalue(command, args.enabledCommands))
env.On("runCommand", command, []string{args.versionParam}).Return(args.version, nil)
env.On("hasCommand", command.executable).Return(args.hasvalue(command.executable, args.enabledCommands))
env.On("runCommand", command.executable, command.args).Return(args.version, args.expectedError)
}
for _, extension := range args.extensions {
env.On("hasFiles", extension).Return(args.hasvalue(extension, args.enabledExtensions))
@ -56,23 +55,23 @@ func bootStrapLanguageTest(args *languageArgs) *language {
props.values[MissingCommandTextProperty] = args.missingCommandText
}
l := &language{
props: props,
env: env,
extensions: args.extensions,
commands: args.commands,
versionParam: args.versionParam,
version: &version{
regex: args.versionRegex,
urlTemplate: args.urlTemplate,
},
props: props,
env: env,
extensions: args.extensions,
commands: args.commands,
versionURLTemplate: args.versionURLTemplate,
}
return l
}
func TestLanguageFilesFoundButNoCommandAndVersionAndDisplayVersion(t *testing.T) {
args := &languageArgs{
commands: []string{"unicorn"},
versionParam: "--version",
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
},
},
extensions: []string{uni},
enabledExtensions: []string{uni},
displayVersion: true,
@ -84,8 +83,12 @@ func TestLanguageFilesFoundButNoCommandAndVersionAndDisplayVersion(t *testing.T)
func TestLanguageFilesFoundButNoCommandAndVersionAndDontDisplayVersion(t *testing.T) {
args := &languageArgs{
commands: []string{"unicorn"},
versionParam: "--version",
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
},
},
extensions: []string{uni},
enabledExtensions: []string{uni},
displayVersion: false,
@ -96,8 +99,12 @@ func TestLanguageFilesFoundButNoCommandAndVersionAndDontDisplayVersion(t *testin
func TestLanguageFilesFoundButNoCommandAndNoVersion(t *testing.T) {
args := &languageArgs{
commands: []string{"unicorn"},
versionParam: "--version",
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
},
},
extensions: []string{uni},
enabledExtensions: []string{uni},
}
@ -107,10 +114,16 @@ func TestLanguageFilesFoundButNoCommandAndNoVersion(t *testing.T) {
func TestLanguageDisabledNoFiles(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"unicorn"},
enabledCommands: []string{"unicorn"},
extensions: []string{uni},
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
},
},
extensions: []string{uni},
enabledExtensions: []string{},
enabledCommands: []string{"unicorn"},
displayVersion: true,
}
lang := bootStrapLanguageTest(args)
assert.False(t, lang.enabled(), "no files in the current directory")
@ -118,12 +131,16 @@ func TestLanguageDisabledNoFiles(t *testing.T) {
func TestLanguageEnabledOneExtensionFound(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"unicorn"},
enabledCommands: []string{"unicorn"},
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
regex: "(?P<version>.*)",
},
},
extensions: []string{uni, corn},
enabledExtensions: []string{uni},
versionRegex: "(?P<version>.*)",
enabledCommands: []string{"unicorn"},
version: universion,
displayVersion: true,
}
@ -134,13 +151,17 @@ func TestLanguageEnabledOneExtensionFound(t *testing.T) {
func TestLanguageEnabledSecondExtensionFound(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"unicorn"},
enabledCommands: []string{"unicorn"},
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
regex: "(?P<version>.*)",
},
},
extensions: []string{uni, corn},
versionRegex: "(?P<version>.*)",
version: universion,
enabledExtensions: []string{corn},
enabledCommands: []string{"unicorn"},
version: universion,
displayVersion: true,
}
lang := bootStrapLanguageTest(args)
@ -150,13 +171,22 @@ func TestLanguageEnabledSecondExtensionFound(t *testing.T) {
func TestLanguageEnabledSecondCommand(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"uni", "corn"},
enabledCommands: []string{"corn"},
commands: []*cmd{
{
executable: "uni",
args: []string{"--version"},
regex: "(?P<version>.*)",
},
{
executable: "corn",
args: []string{"--version"},
regex: "(?P<version>.*)",
},
},
extensions: []string{uni, corn},
versionRegex: "(?P<version>.*)",
version: universion,
enabledExtensions: []string{corn},
enabledCommands: []string{"corn"},
version: universion,
displayVersion: true,
}
lang := bootStrapLanguageTest(args)
@ -166,13 +196,17 @@ func TestLanguageEnabledSecondCommand(t *testing.T) {
func TestLanguageEnabledAllExtensionsFound(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"unicorn"},
enabledCommands: []string{"unicorn"},
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
regex: "(?P<version>.*)",
},
},
extensions: []string{uni, corn},
versionRegex: "(?P<version>.*)",
version: universion,
enabledExtensions: []string{uni, corn},
enabledCommands: []string{"unicorn"},
version: universion,
displayVersion: true,
}
lang := bootStrapLanguageTest(args)
@ -182,13 +216,17 @@ func TestLanguageEnabledAllExtensionsFound(t *testing.T) {
func TestLanguageEnabledNoVersion(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"unicorn"},
enabledCommands: []string{"unicorn"},
commands: []*cmd{
{
executable: "unicorn",
args: []string{"--version"},
regex: "(?P<version>.*)",
},
},
extensions: []string{uni, corn},
versionRegex: "(?P<version>.*)",
version: universion,
enabledExtensions: []string{uni, corn},
enabledCommands: []string{"unicorn"},
version: universion,
displayVersion: false,
}
lang := bootStrapLanguageTest(args)
@ -198,13 +236,11 @@ func TestLanguageEnabledNoVersion(t *testing.T) {
func TestLanguageEnabledMissingCommand(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{""},
enabledCommands: []string{"unicorn"},
commands: []*cmd{},
extensions: []string{uni, corn},
versionRegex: "(?P<version>.*)",
version: universion,
enabledExtensions: []string{uni, corn},
enabledCommands: []string{"unicorn"},
version: universion,
displayVersion: false,
}
lang := bootStrapLanguageTest(args)
@ -214,33 +250,63 @@ func TestLanguageEnabledMissingCommand(t *testing.T) {
func TestLanguageEnabledMissingCommandCustomText(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{""},
enabledCommands: []string{"unicorn"},
commands: []*cmd{},
extensions: []string{uni, corn},
versionRegex: "(?P<version>.*)",
version: universion,
enabledExtensions: []string{uni, corn},
displayVersion: true,
enabledCommands: []string{"unicorn"},
version: universion,
missingCommandText: "missing",
displayVersion: true,
}
lang := bootStrapLanguageTest(args)
assert.True(t, lang.enabled())
assert.Equal(t, args.missingCommandText, lang.string(), "unicorn is available and uni and corn files are found")
}
func TestLanguageEnabledCommandExitCode(t *testing.T) {
expected := 200
args := &languageArgs{
commands: []*cmd{
{
executable: "uni",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
},
extensions: []string{uni, corn},
enabledExtensions: []string{uni, corn},
enabledCommands: []string{"uni"},
version: universion,
displayVersion: true,
expectedError: &commandError{exitCode: expected},
}
lang := bootStrapLanguageTest(args)
assert.True(t, lang.enabled())
assert.Equal(t, "err executing uni with [--version]", lang.string())
assert.Equal(t, expected, lang.exitCode)
}
func TestLanguageHyperlinkEnabled(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"uni", "corn"},
enabledCommands: []string{"corn"},
extensions: []string{uni, corn},
versionRegex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
urlTemplate: "[%s](https://unicor.org/doc/%s.%s.%s)",
version: universion,
enabledExtensions: []string{corn},
displayVersion: true,
enableHyperlink: true,
commands: []*cmd{
{
executable: "uni",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
{
executable: "corn",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
},
versionURLTemplate: "[%s](https://unicor.org/doc/%s.%s.%s)",
extensions: []string{uni, corn},
enabledExtensions: []string{corn},
enabledCommands: []string{"corn"},
version: universion,
displayVersion: true,
enableHyperlink: true,
}
lang := bootStrapLanguageTest(args)
assert.True(t, lang.enabled())
@ -249,34 +315,52 @@ func TestLanguageHyperlinkEnabled(t *testing.T) {
func TestLanguageHyperlinkEnabledWrongRegex(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"uni", "corn"},
enabledCommands: []string{"corn"},
extensions: []string{uni, corn},
versionRegex: `wrong`,
urlTemplate: "[%s](https://unicor.org/doc/%s.%s.%s)",
version: universion,
enabledExtensions: []string{corn},
displayVersion: true,
enableHyperlink: true,
commands: []*cmd{
{
executable: "uni",
args: []string{"--version"},
regex: `wrong`,
},
{
executable: "corn",
args: []string{"--version"},
regex: `wrong`,
},
},
versionURLTemplate: "[%s](https://unicor.org/doc/%s.%s.%s)",
extensions: []string{uni, corn},
enabledExtensions: []string{corn},
enabledCommands: []string{"corn"},
version: universion,
displayVersion: true,
enableHyperlink: true,
}
lang := bootStrapLanguageTest(args)
assert.True(t, lang.enabled())
assert.Equal(t, "", lang.string())
assert.Equal(t, "err parsing info from corn with 1.3.307", lang.string())
}
func TestLanguageHyperlinkEnabledLessParamInTemplate(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"uni", "corn"},
enabledCommands: []string{"corn"},
extensions: []string{uni, corn},
versionRegex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
urlTemplate: "[%s](https://unicor.org/doc/%s)",
version: universion,
enabledExtensions: []string{corn},
displayVersion: true,
enableHyperlink: true,
commands: []*cmd{
{
executable: "uni",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
{
executable: "corn",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
},
versionURLTemplate: "[%s](https://unicor.org/doc/%s)",
extensions: []string{uni, corn},
enabledExtensions: []string{corn},
enabledCommands: []string{"corn"},
version: universion,
displayVersion: true,
enableHyperlink: true,
}
lang := bootStrapLanguageTest(args)
assert.True(t, lang.enabled())
@ -285,16 +369,25 @@ func TestLanguageHyperlinkEnabledLessParamInTemplate(t *testing.T) {
func TestLanguageHyperlinkEnabledMoreParamInTemplate(t *testing.T) {
args := &languageArgs{
versionParam: "--version",
commands: []string{"uni", "corn"},
enabledCommands: []string{"corn"},
extensions: []string{uni, corn},
versionRegex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
urlTemplate: "[%s](https://unicor.org/doc/%s.%s.%s.%s)",
version: universion,
enabledExtensions: []string{corn},
displayVersion: true,
enableHyperlink: true,
commands: []*cmd{
{
executable: "uni",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
{
executable: "corn",
args: []string{"--version"},
regex: `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+)))`,
},
},
versionURLTemplate: "[%s](https://unicor.org/doc/%s.%s.%s.%s)",
extensions: []string{uni, corn},
enabledExtensions: []string{corn},
enabledCommands: []string{"corn"},
version: universion,
displayVersion: true,
enableHyperlink: true,
}
lang := bootStrapLanguageTest(args)
assert.True(t, lang.enabled())

View file

@ -10,15 +10,17 @@ func (n *node) string() string {
func (n *node) init(props *properties, env environmentInfo) {
n.language = &language{
env: env,
props: props,
commands: []string{"node"},
versionParam: "--version",
extensions: []string{"*.js", "*.ts", "package.json"},
version: &version{
regex: `(?:v(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
urlTemplate: "[%[1]s](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V%[2]s.md#%[1]s)",
env: env,
props: props,
extensions: []string{"*.js", "*.ts", "package.json"},
commands: []*cmd{
{
executable: "node",
args: []string{"--version"},
regex: `(?:v(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
},
},
versionURLTemplate: "[%[1]s](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V%[2]s.md#%[1]s)",
}
}

View file

@ -27,17 +27,24 @@ func (p *python) string() string {
func (p *python) init(props *properties, env environmentInfo) {
p.language = &language{
env: env,
props: props,
commands: []string{"python", "python3"},
versionParam: "--version",
extensions: []string{"*.py", "*.ipynb", "pyproject.toml", "venv.bak", "venv", ".venv"},
loadContext: p.loadContext,
inContext: p.inContext,
version: &version{
regex: `(?:Python (?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
urlTemplate: "[%s](https://www.python.org/downloads/release/python-%s%s%s/)",
env: env,
props: props,
extensions: []string{"*.py", "*.ipynb", "pyproject.toml", "venv.bak", "venv", ".venv"},
loadContext: p.loadContext,
inContext: p.inContext,
commands: []*cmd{
{
executable: "python",
args: []string{"--version"},
regex: `(?:Python (?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
},
{
executable: "python3",
args: []string{"--version"},
regex: `(?:Python (?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+).(?P<patch>[0-9]+))))`,
},
},
versionURLTemplate: "[%s](https://www.python.org/downloads/release/python-%s%s%s/)",
}
}