• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mlange-42 / modo / 13180577064

06 Feb 2025 02:07PM CUT coverage: 73.089% (-1.0%) from 74.08%
13180577064

Pull #212

github

web-flow
Merge 98d3dc552 into 1566231bf
Pull Request #212: Add tests for CLI commands

36 of 54 new or added lines in 6 files covered. (66.67%)

2314 of 3166 relevant lines covered (73.09%)

40.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.64
/internal/cmd/util.go
1
package cmd
2

3
import (
4
        "fmt"
5
        "io"
6
        "log"
7
        "os"
8
        "os/exec"
9
        "path"
10
        "path/filepath"
11
        "strings"
12
        "time"
13

14
        "github.com/mlange-42/modo/internal/document"
15
        "github.com/mlange-42/modo/internal/util"
16
        "github.com/rjeczalik/notify"
17
        "github.com/spf13/pflag"
18
        "github.com/spf13/viper"
19
)
20

21
const defaultConfigFile = "modo.yaml"
22
const setExitOnError = "set -e"
23

24
const initFileText = "__init__.mojo"
25
const initFileEmoji = "__init__.🔥"
26

27
var watchExtensions = []string{".md", ".mojo", ".🔥"}
28

29
type Version struct {
30
        Major int
31
        Minor int
32
        Patch int
33
        Dev   bool
34
}
35

36
func (v *Version) Version() string {
1✔
37
        version := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
1✔
38
        if v.Dev {
2✔
39
                version = fmt.Sprintf("%s-dev", version)
1✔
40
        }
1✔
41
        return version
1✔
42
}
43

44
func runCommand(command string) error {
16✔
45
        commandWithExit := fmt.Sprintf("%s\n%s", setExitOnError, command)
16✔
46
        cmd := exec.Command("bash", "-c", commandWithExit)
16✔
47
        cmd.Stdout = os.Stdout
16✔
48
        cmd.Stderr = os.Stderr
16✔
49
        return cmd.Run()
16✔
50
}
16✔
51

52
func runCommands(commands []string) error {
16✔
53
        for _, command := range commands {
32✔
54
                err := runCommand(command)
16✔
55
                if err != nil {
16✔
56
                        return err
×
57
                }
×
58
        }
59
        return nil
16✔
60
}
61

62
func readDocs(file string) (*document.Docs, error) {
3✔
63
        data, err := read(file)
3✔
64
        if err != nil {
3✔
65
                return nil, err
×
66
        }
×
67

68
        if strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml") {
3✔
69
                return document.FromYaml(data)
×
70
        }
×
71

72
        return document.FromJson(data)
3✔
73
}
74

75
func read(file string) ([]byte, error) {
3✔
76
        if file == "" {
3✔
77
                return io.ReadAll(os.Stdin)
×
78
        } else {
3✔
79
                return os.ReadFile(file)
3✔
80
        }
3✔
81
}
82

83
func isPackage(dir string) (isPackage bool, err error) {
27✔
84
        pkgFile := path.Join(dir, initFileText)
27✔
85
        initExists, initIsDir, err := util.FileExists(pkgFile)
27✔
86
        if err != nil {
27✔
87
                return
×
88
        }
×
89
        if initExists && !initIsDir {
31✔
90
                isPackage = true
4✔
91
                return
4✔
92
        }
4✔
93

94
        pkgFile = path.Join(dir, initFileEmoji)
23✔
95
        initExists, initIsDir, err = util.FileExists(pkgFile)
23✔
96
        if err != nil {
23✔
97
                return
×
98
        }
×
99
        if initExists && !initIsDir {
23✔
100
                isPackage = true
×
101
                return
×
102
        }
×
103

104
        return
23✔
105
}
106

107
func mountProject(v *viper.Viper, config string, paths []string) (string, error) {
4✔
108
        cwd, err := os.Getwd()
4✔
109
        if err != nil {
4✔
NEW
110
                return "", err
×
NEW
111
        }
×
112

113
        withConfig := len(paths) > 0
4✔
114
        p := "."
4✔
115
        if withConfig {
8✔
116
                p = paths[0]
4✔
117
                if err := os.Chdir(p); err != nil {
4✔
NEW
118
                        return cwd, err
×
119
                }
×
120
        }
121

122
        exists, isDir, err := util.FileExists(config)
4✔
123
        if err != nil {
4✔
NEW
124
                return cwd, err
×
125
        }
×
126
        if !exists || isDir {
4✔
127
                if withConfig {
×
NEW
128
                        return cwd, fmt.Errorf("no config file '%s' found in path '%s'", config, p)
×
129
                }
×
NEW
130
                return cwd, nil
×
131
        }
132

133
        v.SetConfigName(strings.TrimSuffix(config, path.Ext(config)))
4✔
134
        v.SetConfigType("yaml")
4✔
135
        v.AddConfigPath(".")
4✔
136

4✔
137
        if err := v.ReadInConfig(); err != nil {
4✔
138
                _, notFound := err.(viper.ConfigFileNotFoundError)
×
139
                if !notFound {
×
NEW
140
                        return cwd, err
×
141
                }
×
142
                if withConfig {
×
NEW
143
                        return cwd, err
×
144
                }
×
145
        }
146
        return cwd, nil
4✔
147
}
148

149
type command = func(file string, args *document.Config, form document.Formatter, subdir string, isFile, isDir bool) error
150

151
func runFilesOrDir(cmd command, args *document.Config, form document.Formatter) error {
3✔
152
        if form != nil {
5✔
153
                if err := form.Accepts(args.InputFiles); err != nil {
2✔
154
                        return err
×
155
                }
×
156
        }
157

158
        if len(args.InputFiles) == 0 || (len(args.InputFiles) == 1 && args.InputFiles[0] == "") {
3✔
159
                if err := cmd("", args, form, "", false, false); err != nil {
×
160
                        return err
×
161
                }
×
162
        }
163

164
        stats := make([]struct {
3✔
165
                file bool
3✔
166
                dir  bool
3✔
167
        }, 0, len(args.InputFiles))
3✔
168

3✔
169
        for _, file := range args.InputFiles {
6✔
170
                if s, err := os.Stat(file); err == nil {
6✔
171
                        if s.IsDir() && len(args.InputFiles) > 1 {
3✔
172
                                return fmt.Errorf("only a single directory at a time can be processed")
×
173
                        }
×
174
                        stats = append(stats, struct {
3✔
175
                                file bool
3✔
176
                                dir  bool
3✔
177
                        }{!s.IsDir(), s.IsDir()})
3✔
178
                } else {
×
179
                        return err
×
180
                }
×
181
        }
182

183
        for i, file := range args.InputFiles {
6✔
184
                s := stats[i]
3✔
185
                if err := cmd(file, args, form, "", s.file, s.dir); err != nil {
3✔
186
                        return err
×
187
                }
×
188
        }
189
        return nil
3✔
190
}
191

192
func runDir(baseDir string, args *document.Config, form document.Formatter, runFile command) error {
3✔
193
        baseDir = filepath.Clean(baseDir)
3✔
194

3✔
195
        err := filepath.WalkDir(baseDir,
3✔
196
                func(p string, info os.DirEntry, err error) error {
15✔
197
                        if err != nil {
12✔
198
                                return err
×
199
                        }
×
200
                        if info.IsDir() {
15✔
201
                                return nil
3✔
202
                        }
3✔
203
                        if !strings.HasSuffix(strings.ToLower(p), ".json") {
15✔
204
                                return nil
6✔
205
                        }
6✔
206
                        cleanDir, _ := filepath.Split(path.Clean(p))
3✔
207
                        relDir := filepath.Clean(strings.TrimPrefix(cleanDir, baseDir))
3✔
208
                        return runFile(p, args, form, relDir, true, false)
3✔
209
                })
210
        return err
3✔
211
}
212

213
func commandError(commandType string, err error) error {
×
214
        return fmt.Errorf("in script %s: %s\nTo skip pre- and post-processing scripts, use flag '--bare'", commandType, err)
×
215
}
×
216

217
// bindFlags binds flags to Viper, filtering out the `--watch` and `--config` flag.
218
func bindFlags(v *viper.Viper, flags *pflag.FlagSet) error {
7✔
219
        newFlags := pflag.NewFlagSet("root", pflag.ExitOnError)
7✔
220
        flags.VisitAll(func(f *pflag.Flag) {
90✔
221
                if f.Name == "watch" || f.Name == "config" {
97✔
222
                        return
14✔
223
                }
14✔
224
                newFlags.AddFlag(f)
69✔
225
        })
226
        return v.BindPFlags(newFlags)
7✔
227
}
228

229
func checkConfigFile(f string) error {
8✔
230
        if strings.ContainsRune(f, '/') || strings.ContainsRune(f, '\\') {
8✔
231
                return fmt.Errorf("config file must be in Modo's working directory (as set by the PATH argument)")
×
232
        }
×
233
        return nil
8✔
234
}
235

236
func watchAndRun(args *document.Config, command func(*document.Config) error) error {
×
237
        args.RemovePostScripts()
×
238

×
239
        c := make(chan notify.EventInfo, 32)
×
240
        collected := make(chan []notify.EventInfo, 1)
×
241

×
242
        toWatch, err := getWatchPaths(args)
×
243
        if err != nil {
×
244
                return err
×
245
        }
×
246
        for _, w := range toWatch {
×
247
                if err := notify.Watch(w, c, notify.All); err != nil {
×
248
                        log.Fatal(err)
×
249
                }
×
250
        }
251
        defer notify.Stop(c)
×
252

×
253
        fmt.Printf("Watching for changes: %s\nExit with Ctrl + C\n", strings.Join(toWatch, ", "))
×
254
        ticker := time.NewTicker(1 * time.Second)
×
255
        defer ticker.Stop()
×
256

×
257
        go func() {
×
258
                var events []notify.EventInfo
×
259
                for {
×
260
                        select {
×
261
                        case evt := <-c:
×
262
                                events = append(events, evt)
×
263
                        case <-ticker.C:
×
264
                                if len(events) > 0 {
×
265
                                        collected <- events
×
266
                                        events = nil
×
267
                                } else {
×
268
                                        collected <- nil
×
269
                                }
×
270
                        }
271
                }
272
        }()
273

274
        for events := range collected {
×
275
                if events == nil {
×
276
                        continue
×
277
                }
278
                trigger := false
×
279
                for _, e := range events {
×
280
                        for _, ext := range watchExtensions {
×
281
                                if strings.HasSuffix(e.Path(), ext) {
×
282
                                        trigger = true
×
283
                                        break
×
284
                                }
285
                        }
286
                }
287
                if trigger {
×
288
                        if err := command(args); err != nil {
×
289
                                return err
×
290
                        }
×
291
                        fmt.Printf("Watching for changes: %s\n", strings.Join(toWatch, ", "))
×
292
                }
293
        }
294
        return nil
×
295
}
296

297
func getWatchPaths(args *document.Config) ([]string, error) {
1✔
298
        toWatch := append([]string{}, args.Sources...)
1✔
299
        toWatch = append(toWatch, args.InputFiles...)
1✔
300
        for i, w := range toWatch {
3✔
301
                p := w
2✔
302
                exists, isDir, err := util.FileExists(p)
2✔
303
                if err != nil {
2✔
304
                        return nil, err
×
305
                }
×
306
                if !exists {
2✔
307
                        return nil, fmt.Errorf("file or directory '%s' to watch does not exist", p)
×
308
                }
×
309
                if isDir {
4✔
310
                        p = path.Join(w, "...")
2✔
311
                }
2✔
312
                toWatch[i] = p
2✔
313
        }
314
        return toWatch, nil
1✔
315
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc