ignore those last bits, this time its for real
License: MIT Signed-off-by: Jeromy <why@ipfs.io>
Jeromy committed
Jul 8, 2016 at 14:35 UTC
1aeda7eb6ed21aca3e290f9e9c92c7e5bd942c84
9 files changed
+229
-188
commands/cli/parse.go
+22
-6
@@ -296,10 +296,17 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi
296
stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
297
} else {
298
if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
299
- if err := printReadInfo(stdin, msgStdinInfo); err == nil {
300
- fileArgs[stdin.Name()] = files.NewReaderFile("", stdin.Name(), stdin, nil)
301
- stdin = nil
299
+ fname := ""
300
+ istty, err := isTty(stdin)
301
+ if err != nil {
302
+ return nil, nil, err
303
+ }
304
+ if istty {
305
+ fname = "*stdin*"
306
}
307
+
308
+ fileArgs[stdin.Name()] = files.NewReaderFile(fname, "", stdin, nil)
309
+ stdin = nil
310
}
311
}
312
case cmds.ArgFile:
@@ -417,15 +424,24 @@ func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (fi
424
425
// Inform the user if a file is waiting on input
426
func printReadInfo(f *os.File, msg string) error {
420
- fInfo, err := f.Stat()
427
+ isTty, err := isTty(f)
428
if err != nil {
422
- log.Error(err)
429
return err
430
}
431
426
- if (fInfo.Mode() & os.ModeCharDevice) != 0 {
432
+ if isTty {
433
fmt.Fprintf(os.Stderr, msg, f.Name())
434
}
435
436
return nil
437
}
438
+
439
+func isTty(f *os.File) (bool, error) {
440
+ fInfo, err := f.Stat()
441
+ if err != nil {
442
+ log.Error(err)
443
+ return false, err
444
+ }
445
+
446
+ return (fInfo.Mode() & os.ModeCharDevice) != 0, nil
447
+}
commands/cli/parse_test.go
+60
-81
@@ -4,7 +4,6 @@ import (
4
"io"
5
"io/ioutil"
6
"os"
7
- "path/filepath"
7
"runtime"
8
"strings"
9
"testing"
@@ -178,49 +177,32 @@ func TestArgumentParsing(t *testing.T) {
177
commands.StringArg("b", true, false, "another arg"),
178
},
179
},
181
- "FileArg": {
180
+ "stdinenabled": {
181
Arguments: []commands.Argument{
183
- commands.FileArg("a", true, false, "some arg"),
182
+ commands.StringArg("a", true, true, "some arg").EnableStdin(),
183
},
184
},
186
- "FileArg+Variadic": {
187
- Arguments: []commands.Argument{
188
- commands.FileArg("a", true, true, "some arg"),
189
- },
190
- },
191
- "FileArg+Stdin": {
192
- Arguments: []commands.Argument{
193
- commands.FileArg("a", true, true, "some arg").EnableStdin(),
194
- },
195
- },
196
- "StringArg+FileArg": {
197
- Arguments: []commands.Argument{
198
- commands.StringArg("a", true, false, "some arg"),
199
- commands.FileArg("a", true, false, "some arg"),
200
- },
201
- },
202
- "StringArg+FileArg+Stdin": {
185
+ "stdinenabled2args": &commands.Command{
186
Arguments: []commands.Argument{
187
commands.StringArg("a", true, false, "some arg"),
205
- commands.FileArg("a", true, true, "some arg").EnableStdin(),
188
+ commands.StringArg("b", true, true, "another arg").EnableStdin(),
189
},
190
},
208
- "StringArg+FileArg+Variadic": {
191
+ "stdinenablednotvariadic": &commands.Command{
192
Arguments: []commands.Argument{
210
- commands.StringArg("a", true, false, "some arg"),
211
- commands.FileArg("a", true, true, "some arg"),
193
+ commands.StringArg("a", true, false, "some arg").EnableStdin(),
194
},
195
},
214
- "StringArg+FileArg+Variadic+Stdin": {
196
+ "stdinenablednotvariadic2args": &commands.Command{
197
Arguments: []commands.Argument{
198
commands.StringArg("a", true, false, "some arg"),
217
- commands.FileArg("a", true, true, "some arg"),
199
+ commands.StringArg("b", true, false, "another arg").EnableStdin(),
200
},
201
},
202
},
203
}
204
223
- test := func(cmd words, f *os.File, exp words) {
205
+ test := func(cmd words, f *os.File, res words) {
206
if f != nil {
207
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
208
t.Fatal(err)
@@ -230,18 +212,8 @@ func TestArgumentParsing(t *testing.T) {
212
if err != nil {
213
t.Errorf("Command '%v' should have passed parsing: %v", cmd, err)
214
}
233
-
234
- parsedWords := make([]string, len(req.Arguments()))
235
- copy(parsedWords, req.Arguments())
236
-
237
- if files := req.Files(); files != nil {
238
- for file, err := files.NextFile(); err != io.EOF; file, err = files.NextFile() {
239
- parsedWords = append(parsedWords, file.FullPath())
240
- }
241
- }
242
-
243
- if !sameWords(parsedWords, exp) {
244
- t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, parsedWords, exp)
215
+ if !sameWords(req.Arguments(), res) {
216
+ t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, req.Arguments(), res)
217
}
218
}
219
@@ -281,52 +253,59 @@ func TestArgumentParsing(t *testing.T) {
253
testFail([]string{"reversedoptional"}, nil, "didn't provide any args, 1 required")
254
testFail([]string{"reversedoptional", "value1", "value2", "value3"}, nil, "provided too many args, only takes 1")
255
284
- // Since FileArgs are presently stored ordered by Path, the enum string
285
- // is used to construct a predictably ordered sequence of filenames.
286
- tmpFile := func(t *testing.T, enum string) *os.File {
287
- f, err := ioutil.TempFile("", enum)
256
+ // Use a temp file to simulate stdin
257
+ fileToSimulateStdin := func(t *testing.T, content string) *os.File {
258
+ fstdin, err := ioutil.TempFile("", "")
259
if err != nil {
260
t.Fatal(err)
261
}
291
- fn, err := filepath.EvalSymlinks(f.Name())
292
- if err != nil {
293
- t.Fatal(err)
294
- }
295
- f.Close()
296
- f, err = os.Create(fn)
297
- if err != nil {
262
+ defer os.Remove(fstdin.Name())
263
+
264
+ if _, err := io.WriteString(fstdin, content); err != nil {
265
t.Fatal(err)
266
}
300
-
301
- return f
267
+ return fstdin
268
}
303
- file1 := tmpFile(t, "1")
304
- file2 := tmpFile(t, "2")
305
- file3 := tmpFile(t, "3")
306
- defer os.Remove(file3.Name())
307
- defer os.Remove(file2.Name())
308
- defer os.Remove(file1.Name())
309
-
310
- test([]string{"noarg"}, file1, []string{})
311
- test([]string{"FileArg", file1.Name()}, nil, []string{file1.Name()})
312
- test([]string{"FileArg+Variadic", file1.Name(), file2.Name()}, nil,
313
- []string{file1.Name(), file2.Name()})
314
- test([]string{"FileArg+Stdin"}, file1, []string{file1.Name()})
315
- test([]string{"FileArg+Stdin", "-"}, file1, []string{file1.Name()})
316
- test([]string{"FileArg+Stdin", file1.Name(), "-"}, file2,
317
- []string{file1.Name(), file2.Name()})
318
- test([]string{"StringArg+FileArg",
319
- "foo", file1.Name()}, nil, []string{"foo", file1.Name()})
320
- test([]string{"StringArg+FileArg+Variadic",
321
- "foo", file1.Name(), file2.Name()}, nil,
322
- []string{"foo", file1.Name(), file2.Name()})
323
- test([]string{"StringArg+FileArg+Stdin",
324
- "foo", file1.Name(), "-"}, file2,
325
- []string{"foo", file1.Name(), file2.Name()})
326
- test([]string{"StringArg+FileArg+Variadic+Stdin",
327
- "foo", file1.Name(), file2.Name()}, file3,
328
- []string{"foo", file1.Name(), file2.Name()})
329
- test([]string{"StringArg+FileArg+Variadic+Stdin",
330
- "foo", file1.Name(), file2.Name(), "-"}, file3,
331
- []string{"foo", file1.Name(), file2.Name(), file3.Name()})
269
+
270
+ test([]string{"stdinenabled", "value1", "value2"}, nil, []string{"value1", "value2"})
271
+
272
+ fstdin := fileToSimulateStdin(t, "stdin1")
273
+ test([]string{"stdinenabled"}, fstdin, []string{"stdin1"})
274
+ test([]string{"stdinenabled", "value1"}, fstdin, []string{"value1"})
275
+ test([]string{"stdinenabled", "value1", "value2"}, fstdin, []string{"value1", "value2"})
276
+
277
+ fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
278
+ test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2"})
279
+
280
+ fstdin = fileToSimulateStdin(t, "stdin1\nstdin2\nstdin3")
281
+ test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2", "stdin3"})
282
+
283
+ test([]string{"stdinenabled2args", "value1", "value2"}, nil, []string{"value1", "value2"})
284
+
285
+ fstdin = fileToSimulateStdin(t, "stdin1")
286
+ test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1"})
287
+ test([]string{"stdinenabled2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
288
+ test([]string{"stdinenabled2args", "value1", "value2", "value3"}, fstdin, []string{"value1", "value2", "value3"})
289
+
290
+ fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
291
+ test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1", "stdin2"})
292
+
293
+ test([]string{"stdinenablednotvariadic", "value1"}, nil, []string{"value1"})
294
+
295
+ fstdin = fileToSimulateStdin(t, "stdin1")
296
+ test([]string{"stdinenablednotvariadic"}, fstdin, []string{"stdin1"})
297
+ test([]string{"stdinenablednotvariadic", "value1"}, fstdin, []string{"value1"})
298
+
299
+ test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, nil, []string{"value1", "value2"})
300
+
301
+ fstdin = fileToSimulateStdin(t, "stdin1")
302
+ test([]string{"stdinenablednotvariadic2args", "value1"}, fstdin, []string{"value1", "stdin1"})
303
+ test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
304
+ testFail([]string{"stdinenablednotvariadic2args"}, fstdin, "cant use stdin for non stdin arg")
305
+
306
+ fstdin = fileToSimulateStdin(t, "stdin1")
307
+ test([]string{"noarg"}, fstdin, []string{})
308
+
309
+ fstdin = fileToSimulateStdin(t, "stdin1")
310
+ test([]string{"optionalsecond", "value1", "value2"}, fstdin, []string{"value1", "value2"})
311
}
commands/command.go
+7
-2
@@ -206,7 +206,7 @@ func (c *Command) GetOptions(path []string) (map[string]Option, error) {
206
}
207
208
func (c *Command) CheckArguments(req Request) error {
209
- args := req.Arguments()
209
+ args := req.(*request).arguments
210
211
// count required argument definitions
212
numRequired := 0
@@ -218,7 +218,7 @@ func (c *Command) CheckArguments(req Request) error {
218
219
// iterate over the arg definitions
220
valueIndex := 0 // the index of the current value (in `args`)
221
- for _, argDef := range c.Arguments {
221
+ for i, argDef := range c.Arguments {
222
// skip optional argument definitions if there aren't
223
// sufficient remaining values
224
if len(args)-valueIndex <= numRequired && !argDef.Required ||
@@ -235,6 +235,11 @@ func (c *Command) CheckArguments(req Request) error {
235
valueIndex++
236
}
237
238
+ // in the case of a non-variadic required argument that supports stdin
239
+ if !found && len(c.Arguments)-1 == i && argDef.SupportsStdin {
240
+ found = true
241
+ }
242
+
243
err := checkArgValue(v, found, argDef)
244
if err != nil {
245
return err
commands/request.go
+40
-10
@@ -170,6 +170,16 @@ func (r *request) SetOptions(opts OptMap) error {
170
171
// Arguments returns the arguments slice
172
func (r *request) Arguments() []string {
173
+ if r.haveVarArgsFromStdin() {
174
+ err := r.VarArgs(func(s string) error {
175
+ r.arguments = append(r.arguments, s)
176
+ return nil
177
+ })
178
+ if err != nil && err != io.EOF {
179
+ log.Error(err)
180
+ }
181
+ }
182
+
183
return r.arguments
184
}
185
@@ -189,10 +199,22 @@ func (r *request) Context() context.Context {
199
return r.rctx
200
}
201
202
+func (r *request) haveVarArgsFromStdin() bool {
203
+ // we expect varargs if we have a variadic required argument and no arguments to
204
+ // fill it
205
+ if len(r.cmd.Arguments) == 0 {
206
+ return false
207
+ }
208
+
209
+ last := r.cmd.Arguments[len(r.cmd.Arguments)-1]
210
+ return last.SupportsStdin && last.Type == ArgString &&
211
+ len(r.arguments) < len(r.cmd.Arguments)
212
+}
213
+
214
func (r *request) VarArgs(f func(string) error) error {
215
var i int
216
for i = 0; i < len(r.cmd.Arguments); i++ {
195
- if r.cmd.Arguments[i].Variadic {
217
+ if r.cmd.Arguments[i].Variadic || r.cmd.Arguments[i].SupportsStdin {
218
break
219
}
220
}
@@ -208,19 +230,27 @@ func (r *request) VarArgs(f func(string) error) error {
230
231
return nil
232
} else {
211
- fi, err := r.files.NextFile()
212
- if err != nil {
213
- return err
214
- }
215
-
216
- scan := bufio.NewScanner(fi)
217
- for scan.Scan() {
218
- err := f(scan.Text())
233
+ if r.files != nil {
234
+ fi, err := r.files.NextFile()
235
if err != nil {
236
return err
237
}
238
+
239
+ if fi.FileName() == "*stdin*" {
240
+ fmt.Fprintln(os.Stderr, "ipfs: Reading from stdin; send Ctrl-d to stop.")
241
+ }
242
+
243
+ scan := bufio.NewScanner(fi)
244
+ for scan.Scan() {
245
+ err := f(scan.Text())
246
+ if err != nil {
247
+ return err
248
+ }
249
+ }
250
+ return nil
251
+ } else {
252
+ return fmt.Errorf("expected more arguments from stdin")
253
}
223
- return nil
254
}
255
}
256
core/commands/bitswap.go
+1
-1
@@ -31,7 +31,7 @@ var unwantCmd = &cmds.Command{
31
Tagline: "Remove a given block from your wantlist.",
32
},
33
Arguments: []cmds.Argument{
34
- cmds.StringArg("key", true, true, "Key(s) to remove from your wantlist."),
34
+ cmds.StringArg("key", true, true, "Key(s) to remove from your wantlist.").EnableStdin(),
35
},
36
Run: func(req cmds.Request, res cmds.Response) {
37
nd, err := req.InvocContext().GetNode()
core/commands/block.go
+2
-2
@@ -55,7 +55,7 @@ on raw ipfs blocks. It outputs the following to stdout:
55
},
56
57
Arguments: []cmds.Argument{
58
- cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get."),
58
+ cmds.StringArg("key", true, false, "The base58 multihash of an existing block to stat.").EnableStdin(),
59
},
60
Run: func(req cmds.Request, res cmds.Response) {
61
b, err := getBlockForKey(req, req.Arguments()[0])
@@ -88,7 +88,7 @@ It outputs to stdout, and <key> is a base58 encoded multihash.
88
},
89
90
Arguments: []cmds.Argument{
91
- cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get."),
91
+ cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get.").EnableStdin(),
92
},
93
Run: func(req cmds.Request, res cmds.Response) {
94
b, err := getBlockForKey(req, req.Arguments()[0])
core/commands/bootstrap.go
+23
-20
@@ -47,7 +47,7 @@ in the bootstrap list).
47
},
48
49
Arguments: []cmds.Argument{
50
- cmds.StringArg("peer", false, true, peerOptionDesc),
50
+ cmds.StringArg("peer", false, true, peerOptionDesc).EnableStdin(),
51
},
52
53
Options: []cmds.Option{
@@ -55,30 +55,13 @@ in the bootstrap list).
55
},
56
57
Run: func(req cmds.Request, res cmds.Response) {
58
- inputPeers, err := config.ParseBootstrapPeers(req.Arguments())
59
- if err != nil {
60
- res.SetError(err, cmds.ErrNormal)
61
- return
62
- }
63
-
64
- r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
65
- if err != nil {
66
- res.SetError(err, cmds.ErrNormal)
67
- return
68
- }
69
- defer r.Close()
70
- cfg, err := r.Config()
71
- if err != nil {
72
- res.SetError(err, cmds.ErrNormal)
73
- return
74
- }
75
-
58
deflt, _, err := req.Option("default").Bool()
59
if err != nil {
60
res.SetError(err, cmds.ErrNormal)
61
return
62
}
63
64
+ var inputPeers []config.BootstrapPeer
65
if deflt {
66
// parse separately for meaningful, correct error.
67
defltPeers, err := config.DefaultBootstrapPeers()
@@ -87,7 +70,15 @@ in the bootstrap list).
70
return
71
}
72
90
- inputPeers = append(inputPeers, defltPeers...)
73
+ inputPeers = defltPeers
74
+ } else {
75
+ parsedPeers, err := config.ParseBootstrapPeers(req.Arguments())
76
+ if err != nil {
77
+ res.SetError(err, cmds.ErrNormal)
78
+ return
79
+ }
80
+
81
+ inputPeers = parsedPeers
82
}
83
84
if len(inputPeers) == 0 {
@@ -95,6 +86,18 @@ in the bootstrap list).
86
return
87
}
88
89
+ r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
90
+ if err != nil {
91
+ res.SetError(err, cmds.ErrNormal)
92
+ return
93
+ }
94
+ defer r.Close()
95
+ cfg, err := r.Config()
96
+ if err != nil {
97
+ res.SetError(err, cmds.ErrNormal)
98
+ return
99
+ }
100
+
101
added, err := bootstrapAdd(r, cfg, inputPeers)
102
if err != nil {
103
res.SetError(err, cmds.ErrNormal)
core/commands/pin.go
+22
-66
@@ -61,25 +61,13 @@ var addPinCmd = &cmds.Command{
61
return
62
}
63
64
- out := make(chan interface{})
65
- go func(ctx context.Context) {
66
- defer close(out)
67
- err := req.VarArgs(func(arg string) error {
68
- added, err := corerepo.Pin(n, ctx, []string{arg}, recursive)
69
- if err != nil {
70
- return err
71
- }
72
-
73
- out <- &PinOutput{added}
74
- return nil
75
- })
64
+ added, err := corerepo.Pin(n, req.Context(), req.Arguments(), recursive)
65
+ if err != nil {
66
+ res.SetError(err, cmds.ErrNormal)
67
+ return
68
+ }
69
77
- if err != nil {
78
- res.SetError(err, cmds.ErrNormal)
79
- return
80
- }
81
- }(req.Context())
82
- res.SetOutput((<-chan interface{})(out))
70
+ res.SetOutput(&PinOutput{added})
71
},
72
Marshalers: cmds.MarshalerMap{
73
cmds.Text: func(res cmds.Response) (io.Reader, error) {
@@ -91,28 +79,17 @@ var addPinCmd = &cmds.Command{
79
pintype = "directly"
80
}
81
94
- marshalPinOutput := func(po *PinOutput) io.Reader {
95
- buf := new(bytes.Buffer)
96
- for _, k := range po.Pins {
97
- fmt.Fprintf(buf, "pinned %s %s\n", k, pintype)
98
- }
99
- return buf
100
- }
101
-
102
- out, ok := res.Output().(<-chan interface{})
82
+ po, ok := res.Output().(*PinOutput)
83
if !ok {
84
return nil, u.ErrCast()
85
}
86
107
- marshal := func(i interface{}) (io.Reader, error) {
108
- return marshalPinOutput(i.(*PinOutput)), nil
87
+ buf := new(bytes.Buffer)
88
+ for _, k := range po.Pins {
89
+ fmt.Fprintf(buf, "pinned %s %s\n", k, pintype)
90
}
91
+ return buf, nil
92
111
- return &cmds.ChannelMarshaler{
112
- Res: res,
113
- Marshaler: marshal,
114
- Channel: out,
115
- }, nil
93
},
94
},
95
}
@@ -147,47 +124,26 @@ collected if needed. (By default, recursively. Use -r=false for direct pins)
124
return
125
}
126
150
- out := make(chan interface{})
151
- go func() {
152
- defer close(out)
153
- err = req.VarArgs(func(arg string) error {
154
- removed, err := corerepo.Unpin(n, req.Context(), req.Arguments(), recursive)
155
- if err != nil {
156
- return err
157
- }
158
-
159
- out <- &PinOutput{removed}
160
- return nil
161
- })
162
- if err != nil {
163
- res.SetError(err, cmds.ErrNormal)
164
- return
165
- }
166
- }()
127
+ removed, err := corerepo.Unpin(n, req.Context(), req.Arguments(), recursive)
128
+ if err != nil {
129
+ res.SetError(err, cmds.ErrNormal)
130
+ return
131
+ }
132
168
- res.SetOutput((<-chan interface{})(out))
133
+ res.SetOutput(&PinOutput{removed})
134
},
135
Marshalers: cmds.MarshalerMap{
136
cmds.Text: func(res cmds.Response) (io.Reader, error) {
172
- outch, ok := res.Output().(<-chan interface{})
137
+ added, ok := res.Output().(*PinOutput)
138
if !ok {
139
return nil, u.ErrCast()
140
}
141
177
- marshal := func(i interface{}) (io.Reader, error) {
178
- added := i.(*PinOutput)
179
- buf := new(bytes.Buffer)
180
- for _, k := range added.Pins {
181
- fmt.Fprintf(buf, "unpinned %s\n", k)
182
- }
183
- return buf, nil
142
+ buf := new(bytes.Buffer)
143
+ for _, k := range added.Pins {
144
+ fmt.Fprintf(buf, "unpinned %s\n", k)
145
}
185
-
186
- return &cmds.ChannelMarshaler{
187
- Res: res,
188
- Marshaler: marshal,
189
- Channel: outch,
190
- }, nil
146
+ return buf, nil
147
},
148
},
149
}
test/sharness/t0085-pins.sh
new
+52
@@ -0,0 +1,52 @@
1
+#!/bin/sh
2
+#
3
+# Copyright (c) 2016 Jeromy Johnson
4
+# MIT Licensed; see the LICENSE file in this repository.
5
+#
6
+
7
+test_description="Test ipfs pinning operations"
8
+
9
+. lib/test-lib.sh
10
+
11
+
12
+test_pins() {
13
+ test_expect_success "create some hashes" '
14
+ HASH_A=$(echo "A" | ipfs add -q --pin=false) &&
15
+ HASH_B=$(echo "B" | ipfs add -q --pin=false) &&
16
+ HASH_C=$(echo "C" | ipfs add -q --pin=false) &&
17
+ HASH_D=$(echo "D" | ipfs add -q --pin=false) &&
18
+ HASH_E=$(echo "E" | ipfs add -q --pin=false) &&
19
+ HASH_F=$(echo "F" | ipfs add -q --pin=false) &&
20
+ HASH_G=$(echo "G" | ipfs add -q --pin=false)
21
+ '
22
+
23
+ test_expect_success "put all those hashes in a file" '
24
+ echo $HASH_A > hashes &&
25
+ echo $HASH_B >> hashes &&
26
+ echo $HASH_C >> hashes &&
27
+ echo $HASH_D >> hashes &&
28
+ echo $HASH_E >> hashes &&
29
+ echo $HASH_F >> hashes &&
30
+ echo $HASH_G >> hashes
31
+ '
32
+
33
+ test_expect_success "pin those hashes via stdin" '
34
+ cat hashes | ipfs pin add
35
+ '
36
+
37
+ test_expect_success "unpin those hashes" '
38
+ cat hashes | ipfs pin rm
39
+ '
40
+}
41
+
42
+test_init_ipfs
43
+
44
+test_pins
45
+
46
+test_launch_ipfs_daemon --offline
47
+
48
+test_pins
49
+
50
+test_kill_ipfs_daemon
51
+
52
+test_done