diff --git a/CHANGELOG.md b/CHANGELOG.md index 305af68..8455a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,16 @@ All notable changes to this project will be documented in this file. - +## Unreleased + +### Added + +* Global `zk` configuration at `~/.config/zk/config.toml`. + * Useful to share aliases or default settings across several [notebooks](docs/notebook.md). + * This is the same format as a notebook [configuration file](docs/config.md). + * Shared templates can be stored in `~/.config/zk/templates/`. + * `XDG_CONFIG_HOME` is taken into account. + ## 0.2.1 diff --git a/cmd/container.go b/cmd/container.go index 5d71e75..c6f6fc1 100644 --- a/cmd/container.go +++ b/cmd/container.go @@ -3,7 +3,7 @@ package cmd import ( "io" "os" - "sync" + "path/filepath" "time" "github.com/mickael-menu/zk/adapter/fzf" @@ -22,35 +22,79 @@ import ( ) type Container struct { + Config zk.Config Date date.Provider Logger util.Logger Terminal *term.Terminal templateLoader *handlebars.Loader - - zkOnce sync.Once - zk *zk.Zk - zkErr error + zk *zk.Zk + zkErr error } -func NewContainer() *Container { +func NewContainer() (*Container, error) { + wrap := errors.Wrapper("initialization") + + config := zk.NewDefaultConfig() + + // Load global user config + configPath, err := locateGlobalConfig() + if err != nil { + return nil, wrap(err) + } + if configPath != "" { + config, err = zk.OpenConfig(configPath, config) + if err != nil { + return nil, wrap(err) + } + } + + // Open current notebook + zk, zkErr := zk.Open(".", config) + if zkErr == nil { + config = zk.Config + os.Setenv("ZK_PATH", zk.Path) + } + date := date.NewFrozenNow() return &Container{ + Config: config, Logger: util.NewStdLogger("zk: ", 0), // zk is short-lived, so we freeze the current date to use the same // date for any rendering during the execution. Date: &date, Terminal: term.New(), - } + zk: zk, + zkErr: zkErr, + }, nil } -func (c *Container) OpenZk() (*zk.Zk, error) { - c.zkOnce.Do(func() { - c.zk, c.zkErr = zk.Open(".") - if c.zkErr == nil { - os.Setenv("ZK_PATH", c.zk.Path) +// locateGlobalConfig looks for the global zk config file following the +// XDG Base Directory specification +// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +func locateGlobalConfig() (string, error) { + configHome, ok := os.LookupEnv("XDG_CONFIG_HOME") + if !ok { + home, ok := os.LookupEnv("HOME") + if !ok { + home = "~/" } - }) + configHome = filepath.Join(home, ".config") + } + + configPath := filepath.Join(configHome, "zk/config.toml") + exists, err := paths.Exists(configPath) + switch { + case err != nil: + return "", err + case exists: + return configPath, nil + default: + return "", nil + } +} + +func (c *Container) Zk() (*zk.Zk, error) { return c.zk, c.zkErr } diff --git a/cmd/edit.go b/cmd/edit.go index 9d417d6..1a2ed2a 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -21,7 +21,7 @@ type Edit struct { } func (cmd *Edit) Run(container *Container) error { - zk, err := container.OpenZk() + zk, err := container.Zk() if err != nil { return err } diff --git a/cmd/index.go b/cmd/index.go index 203bb1f..e43b13d 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -15,7 +15,7 @@ func (cmd *Index) Help() string { } func (cmd *Index) Run(container *Container) error { - zk, err := container.OpenZk() + zk, err := container.Zk() if err != nil { return err } diff --git a/cmd/list.go b/cmd/list.go index 0193e35..8931d57 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -29,7 +29,7 @@ func (cmd *List) Run(container *Container) error { cmd.Delimiter = "\x00" } - zk, err := container.OpenZk() + zk, err := container.Zk() if err != nil { return err } diff --git a/cmd/new.go b/cmd/new.go index 4167c37..2fa2504 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -30,7 +30,7 @@ func (cmd *New) ConfigOverrides() zk.ConfigOverrides { } func (cmd *New) Run(container *Container) error { - zk, err := container.OpenZk() + zk, err := container.Zk() if err != nil { return err } @@ -46,6 +46,7 @@ func (cmd *New) Run(container *Container) error { } opts := note.CreateOpts{ + Config: zk.Config, Dir: *dir, Title: opt.NewNotEmptyString(cmd.Title), Content: content, diff --git a/core/note/create.go b/core/note/create.go index 2d7e4bf..e2bf31b 100644 --- a/core/note/create.go +++ b/core/note/create.go @@ -17,6 +17,8 @@ import ( // CreateOpts holds the options to create a new note. type CreateOpts struct { + // Current configuration. + Config zk.Config // Parent directory for the new note. Dir zk.Dir // Title of the note. @@ -52,7 +54,11 @@ func Create( var bodyTemplate templ.Renderer = templ.NullRenderer if templatePath := opts.Dir.Config.Note.BodyTemplatePath.Unwrap(); templatePath != "" { - bodyTemplate, err = templateLoader.LoadFile(templatePath) + absPath, ok := opts.Config.LocateTemplate(templatePath) + if !ok { + return "", wrap(fmt.Errorf("%s: cannot find template", templatePath)) + } + bodyTemplate, err = templateLoader.LoadFile(absPath) if err != nil { return "", wrap(err) } diff --git a/core/zk/config.go b/core/zk/config.go index 93a1bc1..a637932 100644 --- a/core/zk/config.go +++ b/core/zk/config.go @@ -1,10 +1,12 @@ package zk import ( + "io/ioutil" "path/filepath" "github.com/mickael-menu/zk/util/errors" "github.com/mickael-menu/zk/util/opt" + "github.com/mickael-menu/zk/util/paths" toml "github.com/pelletier/go-toml" ) @@ -16,6 +18,37 @@ type Config struct { Tool ToolConfig Aliases map[string]string Extra map[string]string + // Base directories for the relative template paths used in NoteConfig. + TemplatesDirs []string +} + +// NewDefaultConfig creates a new Config with the default settings. +func NewDefaultConfig() Config { + return Config{ + Note: NoteConfig{ + FilenameTemplate: "{{id}}", + Extension: "md", + BodyTemplatePath: opt.NullString, + Lang: "en", + DefaultTitle: "Untitled", + IDOptions: IDOptions{ + Charset: CharsetAlphanum, + Length: 4, + Case: CaseLower, + }, + }, + Groups: map[string]GroupConfig{}, + Format: FormatConfig{ + Markdown: MarkdownConfig{ + Hashtags: true, + ColonTags: false, + MultiwordTags: false, + }, + }, + Aliases: map[string]string{}, + Extra: map[string]string{}, + TemplatesDirs: []string{}, + } } // RootGroupConfig returns the default GroupConfig for the root directory and its descendants. @@ -27,6 +60,31 @@ func (c Config) RootGroupConfig() GroupConfig { } } +// LocateTemplate returns the absolute path for the given template path, by +// looking for it in the templates directories registered in this Config. +func (c Config) LocateTemplate(path string) (string, bool) { + if path == "" { + return "", false + } + + exists := func(path string) bool { + exists, err := paths.Exists(path) + return exists && err == nil + } + + if filepath.IsAbs(path) { + return path, exists(path) + } + + for _, dir := range c.TemplatesDirs { + if candidate := filepath.Join(dir, path); exists(candidate) { + return candidate, true + } + } + + return path, false +} + // FormatConfig holds the configuration for document formats, such as Markdown. type FormatConfig struct { Markdown MarkdownConfig @@ -35,11 +93,11 @@ type FormatConfig struct { // MarkdownConfig holds the configuration for Markdown documents. type MarkdownConfig struct { // Hashtags indicates whether #hashtags are supported. - Hashtags bool `toml:"hashtags" default:"true"` + Hashtags bool // ColonTags indicates whether :colon:tags: are supported. - ColonTags bool `toml:"colon-tags" default:"false"` + ColonTags bool // MultiwordTags indicates whether #multi-word tags# are supported. - MultiwordTags bool `toml:"multiword-tags" default:"false"` + MultiwordTags bool } // ToolConfig holds the external tooling configuration. @@ -107,90 +165,110 @@ func (c *GroupConfig) Override(overrides ConfigOverrides) { } } +// OpenConfig creates a new Config instance from its TOML representation stored +// in the given file. +func OpenConfig(path string, parentConfig Config) (Config, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return parentConfig, errors.Wrapf(err, "failed to open config file at %s", path) + } + + return ParseConfig(content, path, parentConfig) +} + // ParseConfig creates a new Config instance from its TOML representation. -// templatesDir is the base path for the relative templates. -func ParseConfig(content []byte, templatesDir string) (*Config, error) { +// path is the config absolute path, from which will be derived the base path +// for templates. +// +// The parentConfig will be used to inherit default config settings. +func ParseConfig(content []byte, path string, parentConfig Config) (Config, error) { + config := parentConfig + var tomlConf tomlConfig err := toml.Unmarshal(content, &tomlConf) if err != nil { - return nil, errors.Wrap(err, "failed to read config") - } - - root := GroupConfig{ - Paths: []string{}, - Note: NoteConfig{ - FilenameTemplate: "{{id}}", - Extension: "md", - BodyTemplatePath: opt.NullString, - Lang: "en", - DefaultTitle: "Untitled", - IDOptions: IDOptions{ - Charset: CharsetAlphanum, - Length: 4, - Case: CaseLower, - }, - }, - Extra: make(map[string]string), + return config, errors.Wrap(err, "failed to read config") } + // Note note := tomlConf.Note if note.Filename != "" { - root.Note.FilenameTemplate = note.Filename + config.Note.FilenameTemplate = note.Filename } if note.Extension != "" { - root.Note.Extension = note.Extension + config.Note.Extension = note.Extension } if note.Template != "" { - root.Note.BodyTemplatePath = templatePathFromString(note.Template, templatesDir) + config.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template) } if note.IDLength != 0 { - root.Note.IDOptions.Length = note.IDLength + config.Note.IDOptions.Length = note.IDLength } if note.IDCharset != "" { - root.Note.IDOptions.Charset = charsetFromString(note.IDCharset) + config.Note.IDOptions.Charset = charsetFromString(note.IDCharset) } if note.IDCase != "" { - root.Note.IDOptions.Case = caseFromString(note.IDCase) + config.Note.IDOptions.Case = caseFromString(note.IDCase) } if note.Lang != "" { - root.Note.Lang = note.Lang + config.Note.Lang = note.Lang } if note.DefaultTitle != "" { - root.Note.DefaultTitle = note.DefaultTitle + config.Note.DefaultTitle = note.DefaultTitle } if tomlConf.Extra != nil { for k, v := range tomlConf.Extra { - root.Extra[k] = v + config.Extra[k] = v } } - groups := make(map[string]GroupConfig) + // Groups for name, dirTOML := range tomlConf.Groups { - groups[name] = root.merge(dirTOML, name, templatesDir) + parent, ok := config.Groups[name] + if !ok { + parent = config.RootGroupConfig() + } + + config.Groups[name] = parent.merge(dirTOML, name) } - aliases := make(map[string]string) + // Format + markdown := tomlConf.Format.Markdown + if markdown.Hashtags != nil { + config.Format.Markdown.Hashtags = *markdown.Hashtags + } + if markdown.ColonTags != nil { + config.Format.Markdown.ColonTags = *markdown.ColonTags + } + if markdown.MultiwordTags != nil { + config.Format.Markdown.MultiwordTags = *markdown.MultiwordTags + } + + // Tool + tool := tomlConf.Tool + if tool.Editor != nil { + config.Tool.Editor = opt.NewNotEmptyString(*tool.Editor) + } + if tool.Pager != nil { + config.Tool.Pager = opt.NewStringWithPtr(tool.Pager) + } + if tool.FzfPreview != nil { + config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview) + } + + // Aliases if tomlConf.Aliases != nil { for k, v := range tomlConf.Aliases { - aliases[k] = v + config.Aliases[k] = v } } - return &Config{ - Note: root.Note, - Groups: groups, - Format: tomlConf.Format, - Tool: ToolConfig{ - Editor: opt.NewNotEmptyString(tomlConf.Tool.Editor), - Pager: opt.NewStringWithPtr(tomlConf.Tool.Pager), - FzfPreview: opt.NewStringWithPtr(tomlConf.Tool.FzfPreview), - }, - Aliases: aliases, - Extra: root.Extra, - }, nil + config.TemplatesDirs = append([]string{filepath.Join(filepath.Dir(path), "templates")}, config.TemplatesDirs...) + + return config, nil } -func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string, templatesDir string) GroupConfig { +func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string) GroupConfig { res := c.Clone() if tomlConf.Paths != nil { @@ -211,7 +289,7 @@ func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string, templatesDir s res.Note.Extension = note.Extension } if note.Template != "" { - res.Note.BodyTemplatePath = templatePathFromString(note.Template, templatesDir) + res.Note.BodyTemplatePath = opt.NewNotEmptyString(note.Template) } if note.IDLength != 0 { res.Note.IDOptions.Length = note.IDLength @@ -241,7 +319,7 @@ func (c GroupConfig) merge(tomlConf tomlGroupConfig, name string, templatesDir s type tomlConfig struct { Note tomlNoteConfig Groups map[string]tomlGroupConfig `toml:"group"` - Format FormatConfig + Format tomlFormatConfig Tool tomlToolConfig Extra map[string]string Aliases map[string]string `toml:"alias"` @@ -264,8 +342,18 @@ type tomlGroupConfig struct { Extra map[string]string } +type tomlFormatConfig struct { + Markdown tomlMarkdownConfig +} + +type tomlMarkdownConfig struct { + Hashtags *bool `toml:"hashtags"` + ColonTags *bool `toml:"colon-tags"` + MultiwordTags *bool `toml:"multiword-tags"` +} + type tomlToolConfig struct { - Editor string + Editor *string Pager *string FzfPreview *string `toml:"fzf-preview"` } @@ -297,13 +385,3 @@ func caseFromString(c string) Case { return CaseLower } } - -func templatePathFromString(template string, templatesDir string) opt.String { - if template == "" { - return opt.NullString - } - if !filepath.IsAbs(template) { - template = filepath.Join(templatesDir, template) - } - return opt.NewString(template) -} diff --git a/core/zk/config_test.go b/core/zk/config_test.go index 73f38ba..ff5a7ba 100644 --- a/core/zk/config_test.go +++ b/core/zk/config_test.go @@ -2,7 +2,10 @@ package zk import ( "fmt" + "os" + "path/filepath" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/mickael-menu/zk/util/opt" @@ -10,10 +13,10 @@ import ( ) func TestParseDefaultConfig(t *testing.T) { - conf, err := ParseConfig([]byte(""), "") + conf, err := ParseConfig([]byte(""), ".zk/config.toml", NewDefaultConfig()) assert.Nil(t, err) - assert.Equal(t, conf, &Config{ + assert.Equal(t, conf, Config{ Note: NoteConfig{ FilenameTemplate: "{{id}}", Extension: "md", @@ -39,16 +42,15 @@ func TestParseDefaultConfig(t *testing.T) { Pager: opt.NullString, FzfPreview: opt.NullString, }, - Aliases: make(map[string]string), - Extra: make(map[string]string), + Aliases: make(map[string]string), + Extra: make(map[string]string), + TemplatesDirs: []string{".zk/templates"}, }) } func TestParseInvalidConfig(t *testing.T) { - conf, err := ParseConfig([]byte(`;`), "") - + _, err := ParseConfig([]byte(`;`), ".zk/config.toml", NewDefaultConfig()) assert.NotNil(t, err) - assert.Nil(t, conf) } func TestParseComplete(t *testing.T) { @@ -104,10 +106,10 @@ func TestParseComplete(t *testing.T) { [group."without path"] paths = [] - `), "") + `), ".zk/config.toml", NewDefaultConfig()) assert.Nil(t, err) - assert.Equal(t, conf, &Config{ + assert.Equal(t, conf, Config{ Note: NoteConfig{ FilenameTemplate: "{{id}}.note", Extension: "txt", @@ -200,6 +202,7 @@ func TestParseComplete(t *testing.T) { "hello": "world", "salut": "le monde", }, + TemplatesDirs: []string{".zk/templates"}, }) } @@ -231,10 +234,10 @@ func TestParseMergesGroupConfig(t *testing.T) { log-ext = "value" [group.inherited] - `), "") + `), ".zk/config.toml", NewDefaultConfig()) assert.Nil(t, err) - assert.Equal(t, conf, &Config{ + assert.Equal(t, conf, Config{ Note: NoteConfig{ FilenameTemplate: "root-filename", Extension: "txt", @@ -300,6 +303,7 @@ func TestParseMergesGroupConfig(t *testing.T) { "hello": "world", "salut": "le monde", }, + TemplatesDirs: []string{".zk/templates"}, }) } @@ -310,7 +314,7 @@ func TestParsePreservePropertiesAllowingEmptyValues(t *testing.T) { [tool] pager = "" fzf-preview = "" - `), "") + `), ".zk/config.toml", NewDefaultConfig()) assert.Nil(t, err) assert.Equal(t, conf.Tool.Pager.IsNull(), false) @@ -325,7 +329,7 @@ func TestParseIDCharset(t *testing.T) { [note] id-charset = "%v" `, charset) - conf, err := ParseConfig([]byte(toml), "") + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) assert.Nil(t, err) if !cmp.Equal(conf.Note.IDOptions.Charset, expected) { t.Errorf("Didn't parse ID charset `%v` as expected", charset) @@ -346,7 +350,7 @@ func TestParseIDCase(t *testing.T) { [note] id-case = "%v" `, letterCase) - conf, err := ParseConfig([]byte(toml), "") + conf, err := ParseConfig([]byte(toml), ".zk/config.toml", NewDefaultConfig()) assert.Nil(t, err) if !cmp.Equal(conf.Note.IDOptions.Case, expected) { t.Errorf("Didn't parse ID case `%v` as expected", letterCase) @@ -359,21 +363,35 @@ func TestParseIDCase(t *testing.T) { test("unknown", CaseLower) } -func TestParseResolvesTemplatePaths(t *testing.T) { - test := func(template string, expected string) { - toml := fmt.Sprintf(` - [note] - template = "%v" - `, template) - conf, err := ParseConfig([]byte(toml), "/test/.zk/templates") +func TestLocateTemplate(t *testing.T) { + root := fmt.Sprintf("/tmp/zk-test-%d", time.Now().Unix()) + os.Remove(root) + os.MkdirAll(filepath.Join(root, "templates"), os.ModePerm) + + test := func(template string, expected string, exists bool) { + conf, err := ParseConfig([]byte(""), filepath.Join(root, "config.toml"), NewDefaultConfig()) assert.Nil(t, err) - if !cmp.Equal(conf.Note.BodyTemplatePath, opt.NewString(expected)) { - t.Errorf("Didn't resolve template `%v` as expected: %v", template, conf.Note.BodyTemplatePath) + + path, ok := conf.LocateTemplate(template) + if exists { + assert.True(t, ok) + if path != expected { + t.Errorf("Didn't resolve template `%v` as expected: %v", template, expected) + } + } else { + assert.False(t, ok) } } - test("template.tpl", "/test/.zk/templates/template.tpl") - test("/abs/template.tpl", "/abs/template.tpl") + tpl1 := filepath.Join(root, "templates/template.tpl") + test("template.tpl", tpl1, false) + os.Create(tpl1) + test("template.tpl", tpl1, true) + + tpl2 := filepath.Join(root, "abs.tpl") + test(tpl2, tpl2, false) + os.Create(tpl2) + test(tpl2, tpl2, true) } func TestGroupConfigClone(t *testing.T) { diff --git a/core/zk/zk.go b/core/zk/zk.go index 3559f93..493614d 100644 --- a/core/zk/zk.go +++ b/core/zk/zk.go @@ -2,7 +2,6 @@ package zk import ( "fmt" - "io/ioutil" "path/filepath" "strings" @@ -178,7 +177,7 @@ type Dir struct { } // Open locates a notebook at the given path and parses its configuration. -func Open(path string) (*Zk, error) { +func Open(path string, parentConfig Config) (*Zk, error) { wrap := errors.Wrapper("open failed") path, err := filepath.Abs(path) @@ -190,20 +189,14 @@ func Open(path string) (*Zk, error) { return nil, wrap(err) } - configContent, err := ioutil.ReadFile(filepath.Join(path, ".zk/config.toml")) - if err != nil { - return nil, wrap(err) - } - - templatesDir := filepath.Join(path, ".zk/templates") - config, err := ParseConfig(configContent, templatesDir) + config, err := OpenConfig(filepath.Join(path, ".zk/config.toml"), parentConfig) if err != nil { return nil, wrap(err) } return &Zk{ Path: path, - Config: *config, + Config: config, }, nil } diff --git a/docs/config.md b/docs/config.md index c3b892f..ca5dc5b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -12,6 +12,12 @@ Each [notebook](notebook.md) contains a configuration file used to customize you * [`fzf`](tool-fzf.md) * `[alias]` holds your [command aliases](config-alias.md) +## Global configuration file + +You can also create a global configuration file to share aliases and settings across several notebooks. The global configuration is by default located at `~/.config/zk/config.toml`, but you can customize its location with the [`XDG_CONFIG_HOME`](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) environment variable. + +Notebook configuration files will inherit the settings defined in the global configuration file. You can also share templates by storing them under `~/.config/zk/templates/`. + ## Complete example Here's an example of a complete configuration file: diff --git a/main.go b/main.go index 4d8e0cb..e88d0b4 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "strings" "github.com/alecthomas/kong" "github.com/mickael-menu/zk/cmd" @@ -53,7 +54,8 @@ func (cmd *ShowHelp) Run(container *cmd.Container) error { func main() { // Create the dependency graph. - container := cmd.NewContainer() + container, err := cmd.NewContainer() + fatalIfError(err) if isAlias, err := runAlias(container, os.Args[1:]); isAlias { fatalIfError(err) @@ -97,31 +99,33 @@ func fatalIfError(err error) { // runAlias will execute a user alias if the command is one of them. func runAlias(container *cmd.Container, args []string) (bool, error) { + if len(args) < 1 { + return false, nil + } + runningAlias := os.Getenv("ZK_RUNNING_ALIAS") - if zk, err := container.OpenZk(); err == nil && len(args) >= 1 { - for alias, cmdStr := range zk.Config.Aliases { - if alias == runningAlias || alias != args[0] { - continue - } + for alias, cmdStr := range container.Config.Aliases { + if alias == runningAlias || alias != args[0] { + continue + } - // Prevent infinite loop if an alias calls itself. - os.Setenv("ZK_RUNNING_ALIAS", alias) - - cmd := executil.CommandFromString(cmdStr, args[1:]...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - if err, ok := err.(*exec.ExitError); ok { - os.Exit(err.ExitCode()) - return true, nil - } else { - return true, err - } + // Prevent infinite loop if an alias calls itself. + os.Setenv("ZK_RUNNING_ALIAS", alias) + + cmd := executil.CommandFromString(cmdStr, args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + if err, ok := err.(*exec.ExitError); ok { + os.Exit(err.ExitCode()) + return true, nil + } else { + return true, err } - return true, nil } + return true, nil } return false, nil diff --git a/util/test/assert/assert.go b/util/test/assert/assert.go index 92004bc..b8644d6 100644 --- a/util/test/assert/assert.go +++ b/util/test/assert/assert.go @@ -10,6 +10,18 @@ import ( "github.com/mickael-menu/pretty" ) +func True(t *testing.T, value bool) { + if !value { + t.Errorf("Expected to be true") + } +} + +func False(t *testing.T, value bool) { + if value { + t.Errorf("Expected to be false") + } +} + func Nil(t *testing.T, value interface{}) { if !isNil(value) { t.Errorf("Expected `%v` (type %v) to be nil", value, reflect.TypeOf(value))