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

mlange-42 / modo / 14427544817

13 Apr 2025 07:59AM CUT coverage: 74.172% (-0.07%) from 74.237%
14427544817

Pull #234

github

web-flow
Merge 430eb7f6d into 23c3919eb
Pull Request #234: Use new Go 1.24 version feature

2 of 2 new or added lines in 1 file covered. (100.0%)

2375 of 3202 relevant lines covered (74.17%)

48.3 hits per line

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

65.74
/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
func runCommand(command string) error {
22✔
30
        commandWithExit := fmt.Sprintf("%s\n%s", setExitOnError, command)
22✔
31
        cmd := exec.Command("bash", "-c", commandWithExit)
22✔
32
        cmd.Stdout = os.Stdout
22✔
33
        cmd.Stderr = os.Stderr
22✔
34
        return cmd.Run()
22✔
35
}
22✔
36

37
func runCommands(commands []string) error {
22✔
38
        for _, command := range commands {
44✔
39
                err := runCommand(command)
22✔
40
                if err != nil {
22✔
41
                        return err
×
42
                }
×
43
        }
44
        return nil
22✔
45
}
46

47
func readDocs(file string) (*document.Docs, error) {
4✔
48
        data, err := read(file)
4✔
49
        if err != nil {
4✔
50
                return nil, err
×
51
        }
×
52

53
        if strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml") {
4✔
54
                return document.FromYAML(data)
×
55
        }
×
56

57
        return document.FromJSON(data)
4✔
58
}
59

60
func read(file string) ([]byte, error) {
4✔
61
        if file == "" {
4✔
62
                return io.ReadAll(os.Stdin)
×
63
        }
×
64
        return os.ReadFile(file)
4✔
65
}
66

67
func isPackage(dir string) (isPackage bool, err error) {
27✔
68
        pkgFile := path.Join(dir, initFileText)
27✔
69
        initExists, initIsDir, err := util.FileExists(pkgFile)
27✔
70
        if err != nil {
27✔
71
                return
×
72
        }
×
73
        if initExists && !initIsDir {
31✔
74
                isPackage = true
4✔
75
                return
4✔
76
        }
4✔
77

78
        pkgFile = path.Join(dir, initFileEmoji)
23✔
79
        initExists, initIsDir, err = util.FileExists(pkgFile)
23✔
80
        if err != nil {
23✔
81
                return
×
82
        }
×
83
        if initExists && !initIsDir {
23✔
84
                isPackage = true
×
85
                return
×
86
        }
×
87

88
        return
23✔
89
}
90

91
func mountProject(v *viper.Viper, config string, paths []string) (string, error) {
5✔
92
        cwd, err := os.Getwd()
5✔
93
        if err != nil {
5✔
94
                return "", err
×
95
        }
×
96

97
        withConfig := len(paths) > 0
5✔
98
        p := "."
5✔
99
        if withConfig {
10✔
100
                p = paths[0]
5✔
101
                if err := os.Chdir(p); err != nil {
5✔
102
                        return cwd, err
×
103
                }
×
104
        }
105

106
        exists, isDir, err := util.FileExists(config)
5✔
107
        if err != nil {
5✔
108
                return cwd, err
×
109
        }
×
110
        if !exists || isDir {
5✔
111
                if withConfig {
×
112
                        return cwd, fmt.Errorf("no config file '%s' found in path '%s'", config, p)
×
113
                }
×
114
                return cwd, nil
×
115
        }
116

117
        v.SetConfigName(strings.TrimSuffix(config, path.Ext(config)))
5✔
118
        v.SetConfigType("yaml")
5✔
119
        v.AddConfigPath(".")
5✔
120

5✔
121
        if err := v.ReadInConfig(); err != nil {
5✔
122
                _, notFound := err.(viper.ConfigFileNotFoundError)
×
123
                if !notFound {
×
124
                        return cwd, err
×
125
                }
×
126
                if withConfig {
×
127
                        return cwd, err
×
128
                }
×
129
        }
130
        return cwd, nil
5✔
131
}
132

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

135
func runFilesOrDir(cmd command, args *document.Config, form document.Formatter) error {
4✔
136
        if form != nil {
7✔
137
                if err := form.Accepts(args.InputFiles); err != nil {
3✔
138
                        return err
×
139
                }
×
140
        }
141

142
        if len(args.InputFiles) == 0 || (len(args.InputFiles) == 1 && args.InputFiles[0] == "") {
4✔
143
                if err := cmd("", args, form, "", false, false); err != nil {
×
144
                        return err
×
145
                }
×
146
        }
147

148
        stats := make([]struct {
4✔
149
                file bool
4✔
150
                dir  bool
4✔
151
        }, 0, len(args.InputFiles))
4✔
152

4✔
153
        for _, file := range args.InputFiles {
8✔
154
                if s, err := os.Stat(file); err == nil {
8✔
155
                        if s.IsDir() && len(args.InputFiles) > 1 {
4✔
156
                                return fmt.Errorf("only a single directory at a time can be processed")
×
157
                        }
×
158
                        stats = append(stats, struct {
4✔
159
                                file bool
4✔
160
                                dir  bool
4✔
161
                        }{!s.IsDir(), s.IsDir()})
4✔
162
                } else {
×
163
                        return err
×
164
                }
×
165
        }
166

167
        for i, file := range args.InputFiles {
8✔
168
                s := stats[i]
4✔
169
                if err := cmd(file, args, form, "", s.file, s.dir); err != nil {
4✔
170
                        return err
×
171
                }
×
172
        }
173
        return nil
4✔
174
}
175

176
func runDir(baseDir string, args *document.Config, form document.Formatter, runFile command) error {
4✔
177
        baseDir = filepath.Clean(baseDir)
4✔
178

4✔
179
        err := filepath.WalkDir(baseDir,
4✔
180
                func(p string, info os.DirEntry, err error) error {
20✔
181
                        if err != nil {
16✔
182
                                return err
×
183
                        }
×
184
                        if info.IsDir() {
20✔
185
                                return nil
4✔
186
                        }
4✔
187
                        if !strings.HasSuffix(strings.ToLower(p), ".json") {
20✔
188
                                return nil
8✔
189
                        }
8✔
190
                        cleanDir, _ := filepath.Split(path.Clean(p))
4✔
191
                        relDir := filepath.Clean(strings.TrimPrefix(cleanDir, baseDir))
4✔
192
                        return runFile(p, args, form, relDir, true, false)
4✔
193
                })
194
        return err
4✔
195
}
196

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

201
// bindFlags binds flags to Viper, filtering out the `--watch` and `--config` flag.
202
func bindFlags(v *viper.Viper, flags *pflag.FlagSet) error {
8✔
203
        newFlags := pflag.NewFlagSet("root", pflag.ExitOnError)
8✔
204
        flags.VisitAll(func(f *pflag.Flag) {
105✔
205
                if f.Name == "watch" || f.Name == "config" {
113✔
206
                        return
16✔
207
                }
16✔
208
                newFlags.AddFlag(f)
81✔
209
        })
210
        return v.BindPFlags(newFlags)
8✔
211
}
212

213
func checkConfigFile(f string) error {
9✔
214
        if strings.ContainsRune(f, '/') || strings.ContainsRune(f, '\\') {
9✔
215
                return fmt.Errorf("config file must be in Modo's working directory (as set by the PATH argument)")
×
216
        }
×
217
        return nil
9✔
218
}
219

220
func watchAndRun(args *document.Config, command func(*document.Config) error, stop chan struct{}) error {
1✔
221
        args.RemovePostScripts()
1✔
222

1✔
223
        c := make(chan notify.EventInfo, 32)
1✔
224
        collected := make(chan []notify.EventInfo, 1)
1✔
225

1✔
226
        toWatch, err := getWatchPaths(args)
1✔
227
        if err != nil {
1✔
228
                return err
×
229
        }
×
230
        for _, w := range toWatch {
2✔
231
                if err := notify.Watch(w, c, notify.All); err != nil {
1✔
232
                        log.Fatal(err)
×
233
                }
×
234
        }
235
        defer notify.Stop(c)
1✔
236

1✔
237
        fmt.Printf("Watching for changes: %s\nExit with Ctrl + C\n", strings.Join(toWatch, ", "))
1✔
238
        ticker := time.NewTicker(1 * time.Second)
1✔
239
        defer ticker.Stop()
1✔
240

1✔
241
        go func() {
2✔
242
                var events []notify.EventInfo
1✔
243
                for {
6✔
244
                        select {
5✔
245
                        case evt := <-c:
×
246
                                events = append(events, evt)
×
247
                        case <-ticker.C:
4✔
248
                                if len(events) > 0 {
4✔
249
                                        collected <- events
×
250
                                        events = nil
×
251
                                } else {
4✔
252
                                        collected <- nil
4✔
253
                                }
4✔
254
                        }
255
                }
256
        }()
257

258
        for {
6✔
259
                select {
5✔
260
                case events := <-collected:
4✔
261
                        if events == nil {
8✔
262
                                continue
4✔
263
                        }
264
                        trigger := false
×
265
                        for _, e := range events {
×
266
                                for _, ext := range watchExtensions {
×
267
                                        if strings.HasSuffix(e.Path(), ext) {
×
268
                                                trigger = true
×
269
                                                break
×
270
                                        }
271
                                }
272
                        }
273
                        if trigger {
×
274
                                if err := command(args); err != nil {
×
275
                                        return err
×
276
                                }
×
277
                                fmt.Printf("Watching for changes: %s\n", strings.Join(toWatch, ", "))
×
278
                        }
279
                case <-stop:
1✔
280
                        return nil
1✔
281
                }
282
        }
283
}
284

285
func getWatchPaths(args *document.Config) ([]string, error) {
2✔
286
        toWatch := append([]string{}, args.Sources...)
2✔
287
        toWatch = append(toWatch, args.InputFiles...)
2✔
288
        for i, w := range toWatch {
5✔
289
                p := w
3✔
290
                exists, isDir, err := util.FileExists(p)
3✔
291
                if err != nil {
3✔
292
                        return nil, err
×
293
                }
×
294
                if !exists {
3✔
295
                        return nil, fmt.Errorf("file or directory '%s' to watch does not exist", p)
×
296
                }
×
297
                if isDir {
6✔
298
                        p = path.Join(w, "...")
3✔
299
                }
3✔
300
                toWatch[i] = p
3✔
301
        }
302
        return toWatch, nil
2✔
303
}
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