error on unexpected arguments, test out logger, add .milpa

courtesy of the department of departamental recursiveness
This commit is contained in:
Roberto Hidalgo 2023-03-20 22:18:09 -06:00
parent 7fab6d66d8
commit 725347ec48
26 changed files with 635 additions and 210 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
coverage/*

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
milpa dev lint || @milpa.fail "linter has errors"
milpa dev test unit || @milpa.fail "tests failed"

View File

@ -0,0 +1,3 @@
summary: Lints and tests milpa
description: |
runs `milpa dev lint {shell,go}` and `milpa dev test {unit,integration}`

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
cd "$(dirname "$MILPA_COMMAND_REPO")" || @milpa.fail "could not cd into base dir"
@milpa.log info "Linting go files"
golangci-lint run || exit 2
@milpa.log complete "Go files are up to spec"

View File

@ -0,0 +1,3 @@
summary: Runs linter on go files
description: |
basically golangci-lint

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
set -e
if [[ "$MILPA_OPT_COVERAGE" ]]; then
rm -rf coverage
milpa dev test unit --coverage
milpa dev test coverage-report
else
milpa dev test unit
fi

View File

@ -0,0 +1,7 @@
summary: Runs unit and integration tests
description: |
a wrapper for milpa dev test *
options:
coverage:
type: bool
description: if provided, will output coverage reports

View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
runs=()
while IFS= read -r -d $'\0'; do
runs+=("$REPLY")
done < <(find coverage -type d -maxdepth 1 -mindepth 1 -print0)
packages="$(IFS=, ; echo "${runs[*]}")"
@milpa.log info "Building coverage report from runs: ${runs[*]}"
go tool covdata textfmt -i="$packages" -o coverage/coverage.cov || @milpa.fail "could not merge runs"
go tool cover -html=coverage/coverage.cov -o coverage/coverage.html || @milpa.fail "could not build reports"
@milpa.log complete "Coverage report built"
go tool covdata percent -i="$packages"
go tool cover -func=coverage/coverage.cov | tail -n 1

View File

@ -0,0 +1,5 @@
summary: Creates a coverage report from previous test runs
description: |
looks at test/coverage/* for output
see: https://go.dev/testing/coverage/

View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
root="$(dirname "$MILPA_COMMAND_REPO")"
cd "$root" || @milpa.fail "could not cd into $root"
@milpa.log info "Running unit tests"
args=()
if [[ "${MILPA_OPT_COVERAGE}" ]]; then
cover_dir="$root/coverage/unit"
rm -rf "$cover_dir"
mkdir -p "$cover_dir"
args=( -test.gocoverdir="$cover_dir" --coverpkg=./... )
fi
gotestsum --format short -- ./... "${args[@]}" || exit 2
@milpa.log complete "Unit tests passed"

View File

@ -0,0 +1,7 @@
summary: Runs unit tests
description: |
Runs unit tests using gotestsum
options:
coverage:
type: bool
description: if provided, will output coverage reports

View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
# Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
@milpa.load_util user-input
current_branch=$(git rev-parse --abbrev-ref HEAD)
[[ "$current_branch" != "main" ]] && @milpa.fail "Refusing to release on branch <$current_branch>"
[[ -n "$(git status --porcelain)" ]] && @milpa.fail "Git tree is messy, won't continue"
function next_semver() {
local components
IFS="." read -r -a components <<< "${2}"
following=""
case "$1" in
major ) following="$((components[0]+1)).0.0" ;;
minor ) following="${components[0]}.$((components[1]+1)).0" ;;
patch ) following="${components[0]}.${components[1]}.$((components[2]+1))" ;;
*) @milpa.fail "unknown increment type: <$1>"
esac
echo "$following"
}
increment="$MILPA_ARG_INCREMENT"
# get the latest tag, ignoring any pre-releases
# by default current version is 0.0.-1, and must initially release a patch
current="$(git describe --abbrev=0 --exclude='*-*' --tags 2>/dev/null || echo "0.0.-1")"
next=$(next_semver "$increment" "$current")
if [[ "$MILPA_OPT_PRE" ]]; then
# pre releases might update previous ones, look for them
pre_current=$(git describe --abbrev=0 --match="$next-$MILPA_OPT_PRE.*" --tags 2>/dev/null || echo "$current-$MILPA_OPT_PRE.-1")
build=${pre_current##*.}
next="$next-$MILPA_OPT_PRE.$(( build + 1 ))"
fi
@milpa.log info "Creating release with version $(@milpa.fmt inverted "$next")"
@milpa.confirm "Proceed with release?" || @milpa.fail "Refusing to continue, got <$REPLY>"
@milpa.log success "Continuing with release"
@milpa.log info "Creating tag and pushing"
git tag "$next" || @milpa.fail "Could not create tag $next"
git push origin "$next" || @milpa.fail "Could not push tag $next"
@milpa.log complete "Release created and pushed to origin!"

View File

@ -0,0 +1,15 @@
summary: Creates a new tag and updates the changelog
description: |
Automation might trigger a release if github is in a good mood
arguments:
- name: increment
description: "The kind of semver increment"
default: patch
values:
static: [major, minor, patch]
required: true
options:
pre:
values:
static: [alpha, beta, rc]
description: create a pre-release

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Copyright © 2021 Roberto Hidalgo <chinampa@un.rob.mx> // Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
package commands package commands
import ( import (
@ -14,7 +14,8 @@ var GenerateCompletions = &cobra.Command{
Hidden: true, Hidden: true,
DisableAutoGenTag: true, DisableAutoGenTag: true,
SilenceUsage: true, SilenceUsage: true,
Args: cobra.MinimumNArgs(1), Args: cobra.ExactArgs(1),
ValidArgs: []string{"bash", "fish", "zsh"},
RunE: func(cmd *cobra.Command, args []string) (err error) { RunE: func(cmd *cobra.Command, args []string) (err error) {
switch args[0] { switch args[0] {
case "bash": case "bash":

View File

@ -24,7 +24,8 @@ func newCobraRoot(root *command.Command) *cobra.Command {
DisableAutoGenTag: true, DisableAutoGenTag: true,
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
ValidArgs: []string{""}, // This tricks cobra into erroring without a subcommand
ValidArgs: []string{""},
Args: func(cmd *cobra.Command, args []string) error { Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.OnlyValidArgs(cmd, args); err != nil { if err := cobra.OnlyValidArgs(cmd, args); err != nil {
suggestions := []string{} suggestions := []string{}
@ -73,7 +74,9 @@ func ToCobra(cmd *command.Command, globalOptions command.Options) *cobra.Command
Args: func(cc *cobra.Command, supplied []string) error { Args: func(cc *cobra.Command, supplied []string) error {
skipValidation, _ := cc.Flags().GetBool("skip-validation") skipValidation, _ := cc.Flags().GetBool("skip-validation")
if !skipValidation && runtime.ValidationEnabled() { if !skipValidation && runtime.ValidationEnabled() {
cmd.Arguments.Parse(supplied) if err := cmd.Arguments.Parse(supplied); err != nil {
return err
}
return cmd.Arguments.AreValid() return cmd.Arguments.AreValid()
} }
return nil return nil

View File

@ -91,7 +91,9 @@ func Execute(version string) error {
if idx == len(cmd.Path)-1 { if idx == len(cmd.Path)-1 {
leaf := ToCobra(cmd, cmdRoot.Options) leaf := ToCobra(cmd, cmdRoot.Options)
container.AddCommand(leaf) container.AddCommand(leaf)
container.ValidArgs = append(container.ValidArgs, leaf.Name()) if container != ccRoot {
container.ValidArgs = append(container.ValidArgs, leaf.Name())
}
log.Tracef("cobra: %s => %s", leaf.Name(), container.CommandPath()) log.Tracef("cobra: %s => %s", leaf.Name(), container.CommandPath())
break break
} }

View File

@ -51,7 +51,8 @@ func anySliceToStringSlice(src any) []string {
return res return res
} }
func (args *Arguments) Parse(supplied []string) { func (args *Arguments) Parse(supplied []string) error {
parsed := []string{}
for idx, arg := range *args { for idx, arg := range *args {
argumentProvided := idx < len(supplied) argumentProvided := idx < len(supplied)
@ -76,7 +77,13 @@ func (args *Arguments) Parse(supplied []string) {
} else { } else {
arg.SetValue([]string{supplied[idx]}) arg.SetValue([]string{supplied[idx]})
} }
parsed = append(parsed, *arg.provided...)
} }
if len(parsed) != len(supplied) {
return errors.BadArguments{Msg: fmt.Sprintf("Unexpected arguments provided: %s", supplied)}
}
return nil
} }
func (args *Arguments) AreValid() error { func (args *Arguments) AreValid() error {
@ -106,7 +113,9 @@ func (args *Arguments) CompletionFunction(cc *cobra.Command, provided []string,
lastArg := (*args)[len(*args)-1] lastArg := (*args)[len(*args)-1]
hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic hasVariadicArg := expectedArgLen > 0 && lastArg.Variadic
lastArg.Command.Options.Parse(cc.Flags()) lastArg.Command.Options.Parse(cc.Flags())
args.Parse(provided) if err := args.Parse(provided); err != nil {
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
}
directive = cobra.ShellCompDirectiveDefault directive = cobra.ShellCompDirectiveDefault
if argsCompleted < expectedArgLen || hasVariadicArg { if argsCompleted < expectedArgLen || hasVariadicArg {

View File

@ -39,7 +39,7 @@ func testCommand() *Command {
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
cmd := testCommand() cmd := testCommand()
cmd.Arguments.Parse([]string{"asdf", "one", "two", "three"}) cmd.Arguments.Parse([]string{"asdf", "one", "two", "three"}) // nolint: errcheck
known := cmd.Arguments.AllKnown() known := cmd.Arguments.AllKnown()
if !cmd.Arguments[0].IsKnown() { if !cmd.Arguments[0].IsKnown() {
@ -67,7 +67,7 @@ func TestParse(t *testing.T) {
} }
cmd = testCommand() cmd = testCommand()
cmd.Arguments.Parse([]string{"asdf"}) cmd.Arguments.Parse([]string{"asdf"}) // nolint: errcheck
known = cmd.Arguments.AllKnown() known = cmd.Arguments.AllKnown()
if !cmd.Arguments[0].IsKnown() { if !cmd.Arguments[0].IsKnown() {
@ -209,7 +209,7 @@ func TestArgumentsValidate(t *testing.T) {
cmd.Arguments[1] = staticArgument("second", "", []string{"one", "two", "three"}, true) cmd.Arguments[1] = staticArgument("second", "", []string{"one", "two", "three"}, true)
cmd.SetBindings() cmd.SetBindings()
cmd.Arguments.Parse([]string{"first", "one", "three", "two"}) cmd.Arguments.Parse([]string{"first", "one", "three", "two"}) // nolint: errcheck
err := cmd.Arguments.AreValid() err := cmd.Arguments.AreValid()
if err == nil { if err == nil {
@ -219,7 +219,7 @@ func TestArgumentsValidate(t *testing.T) {
for _, c := range cases { for _, c := range cases {
t.Run(c.Command.FullName(), func(t *testing.T) { t.Run(c.Command.FullName(), func(t *testing.T) {
c.Command.Arguments.Parse(c.Args) c.Command.Arguments.Parse(c.Args) // nolint: errcheck
err := c.Command.Arguments.AreValid() err := c.Command.Arguments.AreValid()
if err == nil { if err == nil {
@ -232,144 +232,6 @@ func TestArgumentsValidate(t *testing.T) {
} }
} }
// func TestArgumentsToEnv(t *testing.T) {
// cases := []struct {
// Command *Command
// Args []string
// Expect []string
// Env []string
// }{
// {
// Args: []string{"something"},
// Expect: []string{"export MILPA_ARG_FIRST=something"},
// Command: &Command{
// // Name: []string{"test", "required", "present"},
// Arguments: []*Argument{
// {
// Name: "first",
// Required: true,
// },
// },
// },
// },
// {
// Args: []string{},
// Expect: []string{"export MILPA_ARG_FIRST=default"},
// Command: &Command{
// // Name: []string{"test", "default", "present"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// },
// },
// },
// },
// {
// Args: []string{"zero", "one", "two", "three"},
// Expect: []string{
// "export MILPA_ARG_FIRST=zero",
// "declare -a MILPA_ARG_VARIADIC='( one two three )'",
// },
// Command: &Command{
// // Name: []string{"test", "variadic"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// },
// {
// Name: "variadic",
// Variadic: true,
// },
// },
// },
// },
// {
// Args: []string{},
// Expect: []string{"export MILPA_ARG_FIRST=default"},
// Command: &Command{
// // Name: []string{"test", "static", "default"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// Values: &ValueSource{
// Static: &[]string{
// "default",
// "good",
// },
// },
// },
// },
// },
// },
// {
// Args: []string{"good"},
// Expect: []string{"export MILPA_ARG_FIRST=good"},
// Command: &Command{
// // Name: []string{"test", "static", "good"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// Values: &ValueSource{
// Static: &[]string{
// "default",
// "good",
// },
// },
// },
// },
// },
// },
// {
// Args: []string{"good"},
// Expect: []string{"export MILPA_ARG_FIRST=good"},
// Command: &Command{
// // Name: []string{"test", "script", "good"},
// Arguments: []*Argument{
// {
// Name: "first",
// Default: "default",
// Values: &ValueSource{
// Script: "echo good; echo default",
// },
// },
// },
// },
// },
// }
// for _, c := range cases {
// t.Run(c.Command.FullName(), func(t *testing.T) {
// dst := []string{}
// c.Command.SetBindings()
// c.Command.Arguments.Parse(c.Args)
// c.Command.Arguments.ToEnv(c.Command, &dst, "export ")
// err := c.Command.Arguments.AreValid()
// if err != nil {
// t.Fatalf("Unexpected failure validating: %s", err)
// }
// for _, expected := range c.Expect {
// found := false
// for _, actual := range dst {
// if strings.HasPrefix(actual, expected) {
// found = true
// break
// }
// }
// if !found {
// t.Fatalf("Expected line %v not found in %v", expected, dst)
// }
// }
// })
// }
// }
func TestArgumentToDesc(t *testing.T) { func TestArgumentToDesc(t *testing.T) {
cases := []struct { cases := []struct {
Arg *Argument Arg *Argument

View File

@ -119,7 +119,9 @@ func (cmd *Command) FlagSet() *pflag.FlagSet {
} }
func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error { func (cmd *Command) ParseInput(cc *cobra.Command, args []string) error {
cmd.Arguments.Parse(args) if err := cmd.Arguments.Parse(args); err != nil {
return err
}
skipValidation, _ := cc.Flags().GetBool("skip-validation") skipValidation, _ := cc.Flags().GetBool("skip-validation")
cmd.Options.Parse(cc.Flags()) cmd.Options.Parse(cc.Flags())
if !skipValidation { if !skipValidation {

View File

@ -165,7 +165,9 @@ func (opt *Option) CompletionFunction(cmd *cobra.Command, args []string, toCompl
return return
} }
opt.Command.Arguments.Parse(args) if err := opt.Command.Arguments.Parse(args); err != nil {
return []string{err.Error()}, cobra.ShellCompDirectiveDefault
}
opt.Command.Options.Parse(cmd.Flags()) opt.Command.Options.Parse(cmd.Flags())
var err error var err error

View File

@ -311,7 +311,7 @@ func (vs *ValueSource) UnmarshalYAML(node *yaml.Node) error {
var customCompleters = map[string]CompletionFunc{} var customCompleters = map[string]CompletionFunc{}
// Registers a completion function for the given command.ValueType key name // Registers a completion function for the given command.ValueType key name.
func RegisterValueSource(key string, completion CompletionFunc) { func RegisterValueSource(key string, completion CompletionFunc) {
customCompleters[key] = completion customCompleters[key] = completion
} }

View File

@ -138,7 +138,7 @@ func TestResolveTemplate(t *testing.T) {
}, },
}, },
}).SetBindings() }).SetBindings()
cmd.Arguments.Parse(test.Args) cmd.Arguments.Parse(test.Args) // nolint: errcheck
cmd.Options.Parse(test.Flags) cmd.Options.Parse(test.Flags)
res, err := cmd.ResolveTemplate(test.Tpl, "") res, err := cmd.ResolveTemplate(test.Tpl, "")

View File

@ -1,14 +1,38 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package logger package logger
import ( import (
"fmt"
"strings" "strings"
"time" "time"
"git.rob.mx/nidito/chinampa/pkg/runtime" "git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/fatih/color"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var bold *color.Color
var boldRedBG *color.Color
var boldRed *color.Color
var boldYellowBG *color.Color
var boldYellow *color.Color
var dimmed *color.Color
func init() {
bold = color.New(color.Bold)
bold.EnableColor()
boldRedBG = color.New(color.Bold, color.BgRed)
boldRedBG.EnableColor()
boldRed = color.New(color.Bold, color.FgHiRed)
boldRed.EnableColor()
boldYellowBG = color.New(color.Bold, color.BgYellow, color.FgBlack)
boldYellowBG.EnableColor()
boldYellow = color.New(color.Bold, color.FgHiYellow)
boldYellow.EnableColor()
dimmed = color.New(color.Faint)
dimmed.EnableColor()
}
type Formatter struct { type Formatter struct {
} }
@ -16,7 +40,8 @@ func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) {
prefix := "" prefix := ""
colorEnabled := runtime.ColorEnabled() colorEnabled := runtime.ColorEnabled()
message := entry.Message message := entry.Message
if runtime.VerboseEnabled() { switch {
case runtime.VerboseEnabled():
date := strings.Replace(entry.Time.Local().Format(time.DateTime), " ", "T", 1) date := strings.Replace(entry.Time.Local().Format(time.DateTime), " ", "T", 1)
component := "" component := ""
if c, ok := entry.Data[componentKey]; ok { if c, ok := entry.Data[componentKey]; ok {
@ -24,36 +49,40 @@ func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) {
} }
level := entry.Level.String() level := entry.Level.String()
if colorEnabled { if colorEnabled {
if entry.Level <= logrus.ErrorLevel { switch {
level = "\033[31m\033[1m" + level + "\033[0m" case entry.Level <= logrus.ErrorLevel:
} else if entry.Level == logrus.WarnLevel { level = boldRed.Sprint(level)
level = "\033[33m\033[1m" + level + "\033[0m" case entry.Level == logrus.WarnLevel:
} else if entry.Level >= logrus.DebugLevel && colorEnabled { level = boldYellow.Sprint(level)
message = "\033[2m" + message + "\033[0m" case entry.Level >= logrus.DebugLevel:
level = dimmed.Sprint(level)
message = dimmed.Sprint(message)
default:
level = dimmed.Sprint(level)
} }
} }
prefix = fmt.Sprintf("\033[2m%s %s%s\033[0m\t", date, level, component) prefix = dimmed.Sprint(date) + " " + level + dimmed.Sprint(component) + "\t"
} else if entry.Level == logrus.ErrorLevel { case entry.Level == logrus.ErrorLevel:
if colorEnabled { if colorEnabled {
prefix = "\033[41m\033[1m ERROR \033[0m " prefix = boldRedBG.Sprint(" ERROR ") + " "
} else { } else {
prefix = "ERROR: " prefix = "ERROR: "
} }
} else if entry.Level == logrus.WarnLevel { case entry.Level == logrus.WarnLevel:
if colorEnabled { if colorEnabled {
prefix = "\033[43m\033[31m warning \033[0m " prefix = boldYellowBG.Sprint(" WARNING ") + " "
message = "\033[33m" + message + "\033[0m"
} else { } else {
prefix = "WARNING: " prefix = "WARNING: "
} }
} else if entry.Level >= logrus.DebugLevel { case entry.Level >= logrus.DebugLevel:
if colorEnabled { if colorEnabled {
prefix = "\033[2m" + entry.Level.String() + ":\033[0m " prefix = dimmed.Sprintf("%s: ", strings.ToUpper(entry.Level.String()))
message = "\033[2m" + message + "\033[0m" message = dimmed.Sprint(message)
} else { } else {
prefix = strings.ToUpper(entry.Level.String()) + ": " prefix = strings.ToUpper(entry.Level.String()) + ": "
} }
} }
return []byte(prefix + message + "\n"), nil return []byte(prefix + message + "\n"), nil
} }

187
pkg/logger/log_test.go Normal file
View File

@ -0,0 +1,187 @@
// Copyright © 2022 Roberto Hidalgo <chinampa@un.rob.mx>
// SPDX-License-Identifier: Apache-2.0
package logger_test
import (
"bytes"
"fmt"
"os"
"reflect"
"runtime"
"strings"
"testing"
"time"
. "git.rob.mx/nidito/chinampa/pkg/logger"
rt "git.rob.mx/nidito/chinampa/pkg/runtime"
"github.com/sirupsen/logrus"
)
func withEnv(t *testing.T, env map[string]string) {
prevEnv := os.Environ()
for _, entry := range prevEnv {
parts := strings.SplitN(entry, "=", 2)
os.Unsetenv(parts[0])
}
for k, v := range env {
os.Setenv(k, v)
}
t.Cleanup(func() {
rt.ResetParsedFlags()
for k := range env {
os.Unsetenv(k)
}
for _, entry := range prevEnv {
parts := strings.SplitN(entry, "=", 2)
os.Setenv(parts[0], parts[1])
}
})
}
func TestFormatter(t *testing.T) {
now := strings.Replace(time.Now().Local().Format(time.DateTime), " ", "T", 1)
cases := []struct {
Color bool
Verbose bool
Call func(args ...any)
Expects string
Level logrus.Level
}{
{
Color: true,
Call: Info,
Expects: "message",
Level: logrus.InfoLevel,
},
{
Color: true,
Verbose: true,
Call: Info,
Expects: fmt.Sprintf("\033[2m%s\033[0m \033[2minfo\033[0m\033[2m\033[0m message", now),
Level: logrus.InfoLevel,
},
{
Color: true,
Call: Debug,
Expects: "",
Level: logrus.InfoLevel,
},
{
Call: Debug,
Expects: "DEBUG: message",
Level: logrus.DebugLevel,
},
{
Color: true,
Call: Debug,
Expects: "\033[2mDEBUG: \033[0m\033[2mmessage\033[0m",
Level: logrus.DebugLevel,
},
{
Color: true,
Verbose: true,
Call: Debug,
Expects: fmt.Sprintf("\033[2m%s\033[0m \033[2mdebug\033[0m\033[2m\033[0m\t\033[2mmessage\033[0m",
now),
Level: logrus.DebugLevel,
},
{
Call: Trace,
Expects: "",
Level: logrus.DebugLevel,
},
{
Call: Trace,
Expects: "TRACE: message",
Level: logrus.TraceLevel,
},
{
Call: Warn,
Expects: "",
Level: logrus.ErrorLevel,
},
{
Call: Warn,
Expects: "WARNING: message",
Level: logrus.InfoLevel,
},
{
Call: Warn,
Level: logrus.InfoLevel,
Color: true,
Verbose: true,
Expects: fmt.Sprintf("\033[2m%s\033[0m \033[1;93mwarning\033[0m\033[2m\033[0m\tmessage", now),
},
{
Call: Warn,
Level: logrus.InfoLevel,
Color: true,
Expects: "\033[1;43;30m WARNING \033[0m message",
},
{
Call: Error,
Expects: "ERROR: message",
Level: logrus.ErrorLevel,
},
{
Call: Error,
Expects: "ERROR: message",
Level: logrus.InfoLevel,
},
{
Call: Error,
Level: logrus.InfoLevel,
Color: true,
Verbose: true,
Expects: fmt.Sprintf("\033[2m%s\033[0m \033[1;91merror\033[0m\033[2m\033[0m\tmessage", now),
},
{
Call: Error,
Level: logrus.InfoLevel,
Color: true,
Expects: "\033[1;41m ERROR \033[0m message",
},
}
for _, c := range cases {
fname := runtime.FuncForPC(reflect.ValueOf(c.Call).Pointer()).Name()
comps := []string{fname, c.Level.String()}
if c.Color {
comps = append(comps, "color")
}
if c.Verbose {
comps = append(comps, "verbose")
}
name := strings.Join(comps, "/")
t.Run(name, func(t *testing.T) {
env := map[string]string{
"COLOR": "",
"VERBOSE": "",
}
if c.Color {
env["COLOR"] = "always"
} else {
env["NO_COLOR"] = "1"
}
if c.Verbose {
env["VERBOSE"] = "1"
}
withEnv(t, env)
data := bytes.Buffer{}
logrus.SetLevel(c.Level)
logrus.SetOutput(&data)
c.Call("message")
expected := c.Expects
if c.Expects != "" {
expected = c.Expects + "\n"
}
if res := data.String(); res != expected {
t.Fatalf("%s:\ngot : %s\nwanted: %v", name, res, expected)
}
})
}
}

View File

@ -52,39 +52,72 @@ func isTrueIsh(val string) bool {
return false return false
} }
var _flags map[string]bool
func ResetParsedFlags() {
_flags = nil
}
func flagInArgs(name string) bool {
if _flags == nil {
_flags = map[string]bool{}
for _, arg := range os.Args {
switch arg {
case "--verbose":
_flags["verbose"] = true
delete(_flags, "silent")
case "--silent":
_flags["silent"] = true
delete(_flags, "verbose")
case "--color":
_flags["color"] = true
delete(_flags, "no-color")
case "--no-color":
_flags["no-color"] = true
delete(_flags, "color")
case "--skip-validation":
_flags["skip-validation"] = true
}
}
}
_, ok := _flags[name]
return ok
}
func DebugEnabled() bool { func DebugEnabled() bool {
return isTrueIsh(os.Getenv(env.Debug)) return isTrueIsh(os.Getenv(env.Debug))
} }
func ValidationEnabled() bool { func ValidationEnabled() bool {
return isFalseIsh(os.Getenv(env.ValidationDisabled)) return !flagInArgs("skip-validation") && isFalseIsh(os.Getenv(env.ValidationDisabled))
} }
func VerboseEnabled() bool { func VerboseEnabled() bool {
for _, arg := range os.Args { if flagInArgs("silent") {
if arg == "--verbose" { return false
return true
}
} }
return isTrueIsh(os.Getenv(env.Verbose)) return isTrueIsh(os.Getenv(env.Verbose)) || flagInArgs("verbose")
} }
func SilenceEnabled() bool { func SilenceEnabled() bool {
for _, arg := range os.Args { if flagInArgs("verbose") {
if arg == "--silent" {
return true
}
}
if VerboseEnabled() {
return false return false
} }
if flagInArgs("silent") {
return true
}
return isTrueIsh(os.Getenv(env.Silent)) return isTrueIsh(os.Getenv(env.Silent)) || flagInArgs("silent")
} }
func ColorEnabled() bool { func ColorEnabled() bool {
return isFalseIsh(os.Getenv(env.NoColor)) && !UnstyledHelpEnabled() if flagInArgs("color") {
return true
}
// we're talking to ttys, we want color unless NO_COLOR/--no-color
return !(isTrueIsh(os.Getenv(env.NoColor)) || UnstyledHelpEnabled() || flagInArgs("no-color"))
} }
func UnstyledHelpEnabled() bool { func UnstyledHelpEnabled() bool {

View File

@ -8,15 +8,146 @@ import (
"reflect" "reflect"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"testing" "testing"
"git.rob.mx/nidito/chinampa/pkg/env" "git.rob.mx/nidito/chinampa/pkg/env"
. "git.rob.mx/nidito/chinampa/pkg/runtime" . "git.rob.mx/nidito/chinampa/pkg/runtime"
) )
func TestEnabled(t *testing.T) { func withEnv(t *testing.T, env map[string]string) {
defer func() { os.Setenv(env.Verbose, "") }() prevEnv := os.Environ()
for _, entry := range prevEnv {
parts := strings.SplitN(entry, "=", 2)
os.Unsetenv(parts[0])
}
for k, v := range env {
os.Setenv(k, v)
}
t.Cleanup(func() {
ResetParsedFlags()
for k := range env {
os.Unsetenv(k)
}
for _, entry := range prevEnv {
parts := strings.SplitN(entry, "=", 2)
os.Setenv(parts[0], parts[1])
}
})
}
func TestCombinations(t *testing.T) {
args := append([]string{}, os.Args...)
t.Cleanup(func() { os.Args = args })
cases := []struct {
Env map[string]string
Args []string
Func func() bool
Expects bool
}{
{
Env: map[string]string{},
Args: []string{},
Func: VerboseEnabled,
Expects: false,
},
{
Env: map[string]string{env.Verbose: "1"},
Args: []string{"--silent"},
Func: VerboseEnabled,
Expects: false,
},
{
Env: map[string]string{env.Verbose: "1"},
Args: []string{},
Func: VerboseEnabled,
Expects: true,
},
{
Env: map[string]string{env.Silent: "1"},
Args: []string{},
Func: VerboseEnabled,
Expects: false,
},
{
Env: map[string]string{},
Args: []string{},
Func: SilenceEnabled,
Expects: false,
},
{
Env: map[string]string{env.Silent: "1"},
Args: []string{},
Func: SilenceEnabled,
Expects: true,
},
{
Env: map[string]string{env.Silent: "1"},
Args: []string{"--verbose"},
Func: SilenceEnabled,
Expects: false,
},
{
Env: map[string]string{env.Verbose: "1"},
Args: []string{"--silent"},
Func: SilenceEnabled,
Expects: true,
},
{
Env: map[string]string{env.ForceColor: "1"},
Args: []string{"--no-color"},
Func: ColorEnabled,
Expects: false,
},
{
Env: map[string]string{},
Args: []string{},
Func: ColorEnabled,
Expects: true,
},
{
Env: map[string]string{env.ForceColor: "1"},
Args: []string{},
Func: ColorEnabled,
Expects: true,
},
{
Env: map[string]string{env.ForceColor: "1"},
Args: []string{"--no-color"},
Func: ColorEnabled,
Expects: false,
},
{
Env: map[string]string{env.NoColor: "1"},
Args: []string{},
Func: ColorEnabled,
Expects: false,
},
{
Env: map[string]string{env.NoColor: "1"},
Args: []string{"--color"},
Func: ColorEnabled,
Expects: true,
},
}
for _, c := range cases {
fname := runtime.FuncForPC(reflect.ValueOf(c.Func).Pointer()).Name()
name := fmt.Sprintf("%v/%v/%s", fname, c.Env, c.Args)
t.Run(name, func(t *testing.T) {
withEnv(t, c.Env)
os.Args = c.Args
if res := c.Func(); res != c.Expects {
t.Fatalf("%s got %v wanted: %v", name, res, c.Expects)
}
})
}
}
func TestEnabled(t *testing.T) {
cases := []struct { cases := []struct {
Name string Name string
Func func() bool Func func() bool
@ -27,6 +158,7 @@ func TestEnabled(t *testing.T) {
Func: VerboseEnabled, Func: VerboseEnabled,
Expects: true, Expects: true,
}, },
{ {
Name: env.Silent, Name: env.Silent,
Func: SilenceEnabled, Func: SilenceEnabled,
@ -64,7 +196,7 @@ func TestEnabled(t *testing.T) {
} }
for _, val := range enabled { for _, val := range enabled {
t.Run("enabled-"+val, func(t *testing.T) { t.Run("enabled-"+val, func(t *testing.T) {
os.Setenv(c.Name, val) withEnv(t, map[string]string{c.Name: val})
if c.Func() != c.Expects { if c.Func() != c.Expects {
t.Fatalf("%s wasn't enabled with a valid value: %s", name, val) t.Fatalf("%s wasn't enabled with a valid value: %s", name, val)
} }
@ -74,7 +206,7 @@ func TestEnabled(t *testing.T) {
disabled := []string{"", "no", "false", "0", "disabled"} disabled := []string{"", "no", "false", "0", "disabled"}
for _, val := range disabled { for _, val := range disabled {
t.Run("disabled-"+val, func(t *testing.T) { t.Run("disabled-"+val, func(t *testing.T) {
os.Setenv(c.Name, val) withEnv(t, map[string]string{c.Name: val})
if c.Func() == c.Expects { if c.Func() == c.Expects {
t.Fatalf("%s was enabled with falsy value: %s", name, val) t.Fatalf("%s was enabled with falsy value: %s", name, val)
} }
@ -84,40 +216,46 @@ func TestEnabled(t *testing.T) {
} }
func TestSilent(t *testing.T) { func TestSilent(t *testing.T) {
origArgs := os.Args args := append([]string{}, os.Args...)
t.Cleanup(func() { t.Cleanup(func() { os.Args = args })
os.Args = origArgs
})
t.Run("SILENT = silence", func(t *testing.T) { t.Run("SILENT = silence", func(t *testing.T) {
t.Setenv(env.Silent, "1") withEnv(t, map[string]string{
t.Setenv(env.Verbose, "") env.Silent: "1",
env.Verbose: "",
})
os.Args = []string{} os.Args = []string{}
if !SilenceEnabled() { if !SilenceEnabled() {
t.Fail() t.Fail()
} }
}) })
t.Run("SILENT + VERBOSE = silence", func(t *testing.T) { t.Run("SILENT+VERBOSE=silence", func(t *testing.T) {
t.Setenv(env.Silent, "1") withEnv(t, map[string]string{
t.Setenv(env.Verbose, "1") env.Silent: "1",
env.Verbose: "1",
})
os.Args = []string{} os.Args = []string{}
if SilenceEnabled() { if !SilenceEnabled() {
t.Fail() t.Fail()
} }
}) })
t.Run("VERBOSE + --silent = silent", func(t *testing.T) { t.Run("VERBOSE+--silent=silent", func(t *testing.T) {
t.Setenv(env.Silent, "") withEnv(t, map[string]string{
t.Setenv(env.Verbose, "1") env.Silent: "0",
env.Verbose: "1",
})
os.Args = []string{"some", "random", "--silent", "args"} os.Args = []string{"some", "random", "--silent", "args"}
if !SilenceEnabled() { if !SilenceEnabled() {
t.Fail() t.Fail()
} }
}) })
t.Run("--silent = silent", func(t *testing.T) { t.Run("--silent=silent", func(t *testing.T) {
t.Setenv(env.Silent, "") withEnv(t, map[string]string{
t.Setenv(env.Verbose, "") env.Silent: "",
env.Verbose: "",
})
os.Args = []string{"some", "random", "--silent", "args"} os.Args = []string{"some", "random", "--silent", "args"}
if !SilenceEnabled() { if !SilenceEnabled() {
t.Fail() t.Fail()
@ -125,20 +263,27 @@ func TestSilent(t *testing.T) {
}) })
t.Run("nothing = nothing", func(t *testing.T) { t.Run("nothing = nothing", func(t *testing.T) {
t.Setenv(env.Silent, "") withEnv(t, map[string]string{
t.Setenv(env.Verbose, "") env.Silent: "",
env.Verbose: "",
})
os.Args = []string{"some", "random", "args"} os.Args = []string{"some", "random", "args"}
if SilenceEnabled() { if SilenceEnabled() {
t.Fail() t.Fail()
} }
if VerboseEnabled() {
t.Fail()
}
}) })
} }
func TestEnvironmentMapEnabled(t *testing.T) { func TestEnvironmentMapEnabled(t *testing.T) {
trueString := strconv.FormatBool(true) trueString := strconv.FormatBool(true)
os.Setenv(env.ForceColor, trueString) withEnv(t, map[string]string{
os.Setenv(env.Debug, trueString) env.ForceColor: trueString,
os.Setenv(env.Verbose, trueString) env.Debug: trueString,
env.Verbose: trueString,
})
res := EnvironmentMap() res := EnvironmentMap()
if res == nil { if res == nil {