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

mlange-42 / modo / 13179389384

06 Feb 2025 01:02PM CUT coverage: 56.505% (-17.6%) from 74.08%
13179389384

Pull #212

github

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

1772 of 3136 relevant lines covered (56.51%)

21.11 hits per line

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

0.0
/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 {
×
37
        version := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
×
38
        if v.Dev {
×
39
                version = fmt.Sprintf("%s-dev", version)
×
40
        }
×
41
        return version
×
42
}
43

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

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

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

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

72
        return document.FromJson(data)
×
73
}
74

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

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

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

104
        return
×
105
}
106

107
func mountProject(v *viper.Viper, config string, paths []string) error {
×
108
        withConfig := len(paths) > 0
×
109
        p := "."
×
110
        if withConfig {
×
111
                p = paths[0]
×
112
                if err := os.Chdir(p); err != nil {
×
113
                        return err
×
114
                }
×
115
        }
116

117
        exists, isDir, err := util.FileExists(config)
×
118
        if err != nil {
×
119
                return err
×
120
        }
×
121
        if !exists || isDir {
×
122
                if withConfig {
×
123
                        return fmt.Errorf("no config file '%s' found in path '%s'", config, p)
×
124
                }
×
125
                return nil
×
126
        }
127

128
        v.SetConfigName(strings.TrimSuffix(config, path.Ext(config)))
×
129
        v.SetConfigType("yaml")
×
130
        v.AddConfigPath(".")
×
131

×
132
        if err := v.ReadInConfig(); err != nil {
×
133
                _, notFound := err.(viper.ConfigFileNotFoundError)
×
134
                if !notFound {
×
135
                        return err
×
136
                }
×
137
                if withConfig {
×
138
                        return err
×
139
                }
×
140
        }
141
        return nil
×
142
}
143

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

146
func runFilesOrDir(cmd command, args *document.Config, form document.Formatter) error {
×
147
        if form != nil {
×
148
                if err := form.Accepts(args.InputFiles); err != nil {
×
149
                        return err
×
150
                }
×
151
        }
152

153
        if len(args.InputFiles) == 0 || (len(args.InputFiles) == 1 && args.InputFiles[0] == "") {
×
154
                if err := cmd("", args, form, "", false, false); err != nil {
×
155
                        return err
×
156
                }
×
157
        }
158

159
        stats := make([]struct {
×
160
                file bool
×
161
                dir  bool
×
162
        }, 0, len(args.InputFiles))
×
163

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

178
        for i, file := range args.InputFiles {
×
179
                s := stats[i]
×
180
                if err := cmd(file, args, form, "", s.file, s.dir); err != nil {
×
181
                        return err
×
182
                }
×
183
        }
184
        return nil
×
185
}
186

187
func runDir(baseDir string, args *document.Config, form document.Formatter, runFile command) error {
×
188
        baseDir = filepath.Clean(baseDir)
×
189

×
190
        err := filepath.WalkDir(baseDir,
×
191
                func(p string, info os.DirEntry, err error) error {
×
192
                        if err != nil {
×
193
                                return err
×
194
                        }
×
195
                        if info.IsDir() {
×
196
                                return nil
×
197
                        }
×
198
                        if !strings.HasSuffix(strings.ToLower(p), ".json") {
×
199
                                return nil
×
200
                        }
×
201
                        cleanDir, _ := filepath.Split(path.Clean(p))
×
202
                        relDir := filepath.Clean(strings.TrimPrefix(cleanDir, baseDir))
×
203
                        return runFile(p, args, form, relDir, true, false)
×
204
                })
205
        return err
×
206
}
207

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

212
// bindFlags binds flags to Viper, filtering out the `--watch` and `--config` flag.
213
func bindFlags(v *viper.Viper, flags *pflag.FlagSet) error {
×
214
        newFlags := pflag.NewFlagSet("root", pflag.ExitOnError)
×
215
        flags.VisitAll(func(f *pflag.Flag) {
×
216
                if f.Name == "watch" || f.Name == "config" {
×
217
                        return
×
218
                }
×
219
                newFlags.AddFlag(f)
×
220
        })
221
        return v.BindPFlags(newFlags)
×
222
}
223

224
func checkConfigFile(f string) error {
×
225
        if strings.ContainsRune(f, '/') || strings.ContainsRune(f, '\\') {
×
226
                return fmt.Errorf("config file must be in Modo's working directory (as set by the PATH argument)")
×
227
        }
×
228
        return nil
×
229
}
230

231
func watchAndRun(args *document.Config, command func(*document.Config) error) error {
×
232
        args.RemovePostScripts()
×
233

×
234
        c := make(chan notify.EventInfo, 32)
×
235
        collected := make(chan []notify.EventInfo, 1)
×
236

×
237
        toWatch, err := getWatchPaths(args)
×
238
        if err != nil {
×
239
                return err
×
240
        }
×
241
        for _, w := range toWatch {
×
242
                if err := notify.Watch(w, c, notify.All); err != nil {
×
243
                        log.Fatal(err)
×
244
                }
×
245
        }
246
        defer notify.Stop(c)
×
247

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

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

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

292
func getWatchPaths(args *document.Config) ([]string, error) {
×
293
        toWatch := append([]string{}, args.Sources...)
×
294
        toWatch = append(toWatch, args.InputFiles...)
×
295
        for i, w := range toWatch {
×
296
                p := w
×
297
                exists, isDir, err := util.FileExists(p)
×
298
                if err != nil {
×
299
                        return nil, err
×
300
                }
×
301
                if !exists {
×
302
                        return nil, fmt.Errorf("file or directory '%s' to watch does not exist", p)
×
303
                }
×
304
                if isDir {
×
305
                        p = path.Join(w, "...")
×
306
                }
×
307
                toWatch[i] = p
×
308
        }
309
        return toWatch, nil
×
310
}
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