]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_cli.py
test_cli: more coverage for 'tahoe put' modifying a mutable file in-place, by filenam...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_cli.py
1
2 import os.path
3 from twisted.trial import unittest
4 from cStringIO import StringIO
5 import urllib
6
7 from allmydata.util import fileutil, hashutil
8 from allmydata import uri
9
10 # at least import the CLI scripts, even if we don't have any real tests for
11 # them yet.
12 from allmydata.scripts import tahoe_ls, tahoe_get, tahoe_put, tahoe_rm
13 from allmydata.scripts.common import DEFAULT_ALIAS, get_aliases
14 _hush_pyflakes = [tahoe_ls, tahoe_get, tahoe_put, tahoe_rm]
15
16 from allmydata.scripts import cli, debug, runner
17 from allmydata.test.common import SystemTestMixin
18 from twisted.internet import threads # CLI tests use deferToThread
19
20 class CLI(unittest.TestCase):
21     # this test case only looks at argument-processing and simple stuff.
22     def test_options(self):
23         fileutil.rm_dir("cli/test_options")
24         fileutil.make_dirs("cli/test_options")
25         fileutil.make_dirs("cli/test_options/private")
26         open("cli/test_options/node.url","w").write("http://localhost:8080/\n")
27         filenode_uri = uri.WriteableSSKFileURI(writekey="\x00"*16,
28                                                fingerprint="\x00"*32)
29         private_uri = uri.NewDirectoryURI(filenode_uri).to_string()
30         open("cli/test_options/private/root_dir.cap", "w").write(private_uri + "\n")
31         o = cli.ListOptions()
32         o.parseOptions(["--node-directory", "cli/test_options"])
33         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
34         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
35         self.failUnlessEqual(o.where, "")
36
37         o = cli.ListOptions()
38         o.parseOptions(["--node-directory", "cli/test_options",
39                         "--node-url", "http://example.org:8111/"])
40         self.failUnlessEqual(o['node-url'], "http://example.org:8111/")
41         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], private_uri)
42         self.failUnlessEqual(o.where, "")
43
44         o = cli.ListOptions()
45         o.parseOptions(["--node-directory", "cli/test_options",
46                         "--dir-cap", "root"])
47         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
48         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], "root")
49         self.failUnlessEqual(o.where, "")
50
51         o = cli.ListOptions()
52         other_filenode_uri = uri.WriteableSSKFileURI(writekey="\x11"*16,
53                                                      fingerprint="\x11"*32)
54         other_uri = uri.NewDirectoryURI(other_filenode_uri).to_string()
55         o.parseOptions(["--node-directory", "cli/test_options",
56                         "--dir-cap", other_uri])
57         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
58         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
59         self.failUnlessEqual(o.where, "")
60
61         o = cli.ListOptions()
62         o.parseOptions(["--node-directory", "cli/test_options",
63                         "--dir-cap", other_uri, "subdir"])
64         self.failUnlessEqual(o['node-url'], "http://localhost:8080/")
65         self.failUnlessEqual(o.aliases[DEFAULT_ALIAS], other_uri)
66         self.failUnlessEqual(o.where, "subdir")
67
68     def _dump_cap(self, *args):
69         out,err = StringIO(), StringIO()
70         config = debug.DumpCapOptions()
71         config.parseOptions(args)
72         debug.dump_cap(config, out, err)
73         self.failIf(err.getvalue())
74         output = out.getvalue()
75         return output
76
77     def test_dump_cap_chk(self):
78         key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
79         storage_index = hashutil.storage_index_hash(key)
80         uri_extension_hash = hashutil.uri_extension_hash("stuff")
81         needed_shares = 25
82         total_shares = 100
83         size = 1234
84         u = uri.CHKFileURI(key=key,
85                            uri_extension_hash=uri_extension_hash,
86                            needed_shares=needed_shares,
87                            total_shares=total_shares,
88                            size=size)
89         output = self._dump_cap(u.to_string())
90         self.failUnless("CHK File:" in output, output)
91         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
92         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
93         self.failUnless("size: 1234" in output, output)
94         self.failUnless("k/N: 25/100" in output, output)
95         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
96
97         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
98                                 u.to_string())
99         self.failUnless("client renewal secret: znxmki5zdibb5qlt46xbdvk2t55j7hibejq3i5ijyurkr6m6jkhq" in output, output)
100
101         output = self._dump_cap(u.get_verifier().to_string())
102         self.failIf("key: " in output, output)
103         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
104         self.failUnless("size: 1234" in output, output)
105         self.failUnless("k/N: 25/100" in output, output)
106         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
107
108         prefixed_u = "http://127.0.0.1/uri/%s" % urllib.quote(u.to_string())
109         output = self._dump_cap(prefixed_u)
110         self.failUnless("CHK File:" in output, output)
111         self.failUnless("key: aaaqeayeaudaocajbifqydiob4" in output, output)
112         self.failUnless("UEB hash: nf3nimquen7aeqm36ekgxomalstenpkvsdmf6fplj7swdatbv5oa" in output, output)
113         self.failUnless("size: 1234" in output, output)
114         self.failUnless("k/N: 25/100" in output, output)
115         self.failUnless("storage index: hdis5iaveku6lnlaiccydyid7q" in output, output)
116
117     def test_dump_cap_lit(self):
118         u = uri.LiteralFileURI("this is some data")
119         output = self._dump_cap(u.to_string())
120         self.failUnless("Literal File URI:" in output, output)
121         self.failUnless("data: this is some data" in output, output)
122
123     def test_dump_cap_ssk(self):
124         writekey = "\x01" * 16
125         fingerprint = "\xfe" * 32
126         u = uri.WriteableSSKFileURI(writekey, fingerprint)
127
128         output = self._dump_cap(u.to_string())
129         self.failUnless("SSK Writeable URI:" in output, output)
130         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output, output)
131         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
132         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
133         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
134
135         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
136                                 u.to_string())
137         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
138
139         fileutil.make_dirs("cli/test_dump_cap/private")
140         f = open("cli/test_dump_cap/private/secret", "w")
141         f.write("5s33nk3qpvnj2fw3z4mnm2y6fa\n")
142         f.close()
143         output = self._dump_cap("--client-dir", "cli/test_dump_cap",
144                                 u.to_string())
145         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
146
147         output = self._dump_cap("--client-dir", "cli/test_dump_cap_BOGUS",
148                                 u.to_string())
149         self.failIf("file renewal secret:" in output, output)
150
151         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
152                                 u.to_string())
153         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
154         self.failIf("file renewal secret:" in output, output)
155
156         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
157                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
158                                 u.to_string())
159         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
160         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
161         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
162
163         u = u.get_readonly()
164         output = self._dump_cap(u.to_string())
165         self.failUnless("SSK Read-only URI:" in output, output)
166         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
167         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
168         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
169
170         u = u.get_verifier()
171         output = self._dump_cap(u.to_string())
172         self.failUnless("SSK Verifier URI:" in output, output)
173         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
174         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
175
176     def test_dump_cap_directory(self):
177         writekey = "\x01" * 16
178         fingerprint = "\xfe" * 32
179         u1 = uri.WriteableSSKFileURI(writekey, fingerprint)
180         u = uri.NewDirectoryURI(u1)
181
182         output = self._dump_cap(u.to_string())
183         self.failUnless("Directory Writeable URI:" in output, output)
184         self.failUnless("writekey: aeaqcaibaeaqcaibaeaqcaibae" in output,
185                         output)
186         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
187         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output,
188                         output)
189         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
190
191         output = self._dump_cap("--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
192                                 u.to_string())
193         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
194
195         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
196                                 u.to_string())
197         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
198         self.failIf("file renewal secret:" in output, output)
199
200         output = self._dump_cap("--nodeid", "tqc35esocrvejvg4mablt6aowg6tl43j",
201                                 "--client-secret", "5s33nk3qpvnj2fw3z4mnm2y6fa",
202                                 u.to_string())
203         self.failUnless("write_enabler: mgcavriox2wlb5eer26unwy5cw56elh3sjweffckkmivvsxtaknq" in output, output)
204         self.failUnless("file renewal secret: arpszxzc2t6kb4okkg7sp765xgkni5z7caavj7lta73vmtymjlxq" in output, output)
205         self.failUnless("lease renewal secret: 7pjtaumrb7znzkkbvekkmuwpqfjyfyamznfz4bwwvmh4nw33lorq" in output, output)
206
207         u = u.get_readonly()
208         output = self._dump_cap(u.to_string())
209         self.failUnless("Directory Read-only URI:" in output, output)
210         self.failUnless("readkey: nvgh5vj2ekzzkim5fgtb4gey5y" in output, output)
211         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
212         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
213
214         u = u.get_verifier()
215         output = self._dump_cap(u.to_string())
216         self.failUnless("Directory Verifier URI:" in output, output)
217         self.failUnless("storage index: nt4fwemuw7flestsezvo2eveke" in output, output)
218         self.failUnless("fingerprint: 737p57x6737p57x6737p57x6737p57x6737p57x6737p57x6737a" in output, output)
219
220 class CLITestMixin:
221     def do_cli(self, verb, *args, **kwargs):
222         nodeargs = [
223             "--node-directory", self.getdir("client0"),
224             ]
225         argv = [verb] + nodeargs + list(args)
226         stdin = kwargs.get("stdin", "")
227         stdout, stderr = StringIO(), StringIO()
228         d = threads.deferToThread(runner.runner, argv, run_by_human=False,
229                                   stdin=StringIO(stdin),
230                                   stdout=stdout, stderr=stderr)
231         def _done(res):
232             return stdout.getvalue(), stderr.getvalue()
233         d.addCallback(_done)
234         return d
235
236 class CreateAlias(SystemTestMixin, CLITestMixin, unittest.TestCase):
237
238     def test_create(self):
239         self.basedir = os.path.dirname(self.mktemp())
240         d = self.set_up_nodes()
241         d.addCallback(lambda res: self.do_cli("create-alias", "tahoe"))
242         def _done((stdout,stderr)):
243             self.failUnless("Alias 'tahoe' created" in stdout)
244             self.failIf(stderr)
245             aliases = get_aliases(self.getdir("client0"))
246             self.failUnless("tahoe" in aliases)
247             self.failUnless(aliases["tahoe"].startswith("URI:DIR2:"))
248         d.addCallback(_done)
249         return d
250
251 class Put(SystemTestMixin, CLITestMixin, unittest.TestCase):
252
253     def test_unlinked_immutable_stdin(self):
254         # tahoe get `echo DATA | tahoe put`
255         # tahoe get `echo DATA | tahoe put -`
256
257         self.basedir = self.mktemp()
258         DATA = "data" * 100
259         d = self.set_up_nodes()
260         d.addCallback(lambda res: self.do_cli("put", stdin=DATA))
261         def _uploaded(res):
262             (stdout, stderr) = res
263             self.failUnless("waiting for file data on stdin.." in stderr)
264             self.failUnless("200 OK" in stderr)
265             self.readcap = stdout
266             self.failUnless(self.readcap.startswith("URI:CHK:"))
267         d.addCallback(_uploaded)
268         d.addCallback(lambda res: self.do_cli("get", self.readcap))
269         def _downloaded(res):
270             (stdout, stderr) = res
271             self.failUnlessEqual(stderr, "")
272             self.failUnlessEqual(stdout, DATA)
273         d.addCallback(_downloaded)
274         d.addCallback(lambda res: self.do_cli("put", "-", stdin=DATA))
275         d.addCallback(lambda (stdout,stderr):
276                       self.failUnlessEqual(stdout, self.readcap))
277         return d
278
279     def test_unlinked_immutable_from_file(self):
280         # tahoe put file.txt
281         # tahoe put ./file.txt
282         # tahoe put /tmp/file.txt
283         # tahoe put ~/file.txt
284         self.basedir = os.path.dirname(self.mktemp())
285         # this will be "allmydata.test.test_cli/Put/test_put_from_file/RANDOM"
286         # and the RANDOM directory will exist. Raw mktemp returns a filename.
287
288         rel_fn = os.path.join(self.basedir, "DATAFILE")
289         abs_fn = os.path.abspath(rel_fn)
290         # we make the file small enough to fit in a LIT file, for speed
291         f = open(rel_fn, "w")
292         f.write("short file")
293         f.close()
294         d = self.set_up_nodes()
295         d.addCallback(lambda res: self.do_cli("put", rel_fn))
296         def _uploaded((stdout,stderr)):
297             readcap = stdout
298             self.failUnless(readcap.startswith("URI:LIT:"))
299             self.readcap = readcap
300         d.addCallback(_uploaded)
301         d.addCallback(lambda res: self.do_cli("put", "./" + rel_fn))
302         d.addCallback(lambda (stdout,stderr):
303                       self.failUnlessEqual(stdout, self.readcap))
304         d.addCallback(lambda res: self.do_cli("put", abs_fn))
305         d.addCallback(lambda (stdout,stderr):
306                       self.failUnlessEqual(stdout, self.readcap))
307         # we just have to assume that ~ is handled properly
308         return d
309
310     def test_immutable_from_file(self):
311         # tahoe put file.txt uploaded.txt
312         # tahoe - uploaded.txt
313         # tahoe put file.txt subdir/uploaded.txt
314         # tahoe put file.txt tahoe:uploaded.txt
315         # tahoe put file.txt tahoe:subdir/uploaded.txt
316         # tahoe put file.txt DIRCAP:./uploaded.txt
317         # tahoe put file.txt DIRCAP:./subdir/uploaded.txt
318         self.basedir = os.path.dirname(self.mktemp())
319
320         rel_fn = os.path.join(self.basedir, "DATAFILE")
321         abs_fn = os.path.abspath(rel_fn)
322         # we make the file small enough to fit in a LIT file, for speed
323         DATA = "short file"
324         DATA2 = "short file two"
325         f = open(rel_fn, "w")
326         f.write(DATA)
327         f.close()
328
329         d = self.set_up_nodes()
330         d.addCallback(lambda res: self.do_cli("create-alias", "tahoe"))
331
332         d.addCallback(lambda res:
333                       self.do_cli("put", rel_fn, "uploaded.txt"))
334         def _uploaded((stdout,stderr)):
335             readcap = stdout.strip()
336             self.failUnless(readcap.startswith("URI:LIT:"))
337             self.failUnless("201 Created" in stderr, stderr)
338             self.readcap = readcap
339         d.addCallback(_uploaded)
340         d.addCallback(lambda res:
341                       self.do_cli("get", "tahoe:uploaded.txt"))
342         d.addCallback(lambda (stdout,stderr):
343                       self.failUnlessEqual(stdout, DATA))
344
345         d.addCallback(lambda res:
346                       self.do_cli("put", "-", "uploaded.txt", stdin=DATA2))
347         def _replaced((stdout,stderr)):
348             readcap = stdout.strip()
349             self.failUnless(readcap.startswith("URI:LIT:"))
350             self.failUnless("200 OK" in stderr, stderr)
351         d.addCallback(_replaced)
352
353         d.addCallback(lambda res:
354                       self.do_cli("put", rel_fn, "subdir/uploaded2.txt"))
355         d.addCallback(lambda res: self.do_cli("get", "subdir/uploaded2.txt"))
356         d.addCallback(lambda (stdout,stderr):
357                       self.failUnlessEqual(stdout, DATA))
358
359         d.addCallback(lambda res:
360                       self.do_cli("put", rel_fn, "tahoe:uploaded3.txt"))
361         d.addCallback(lambda res: self.do_cli("get", "tahoe:uploaded3.txt"))
362         d.addCallback(lambda (stdout,stderr):
363                       self.failUnlessEqual(stdout, DATA))
364
365         d.addCallback(lambda res:
366                       self.do_cli("put", rel_fn, "tahoe:subdir/uploaded4.txt"))
367         d.addCallback(lambda res:
368                       self.do_cli("get", "tahoe:subdir/uploaded4.txt"))
369         d.addCallback(lambda (stdout,stderr):
370                       self.failUnlessEqual(stdout, DATA))
371
372         def _get_dircap(res):
373             self.dircap = get_aliases(self.getdir("client0"))["tahoe"]
374         d.addCallback(_get_dircap)
375
376         d.addCallback(lambda res:
377                       self.do_cli("put", rel_fn,
378                                   self.dircap+":./uploaded5.txt"))
379         d.addCallback(lambda res:
380                       self.do_cli("get", "tahoe:uploaded5.txt"))
381         d.addCallback(lambda (stdout,stderr):
382                       self.failUnlessEqual(stdout, DATA))
383
384         d.addCallback(lambda res:
385                       self.do_cli("put", rel_fn,
386                                   self.dircap+":./subdir/uploaded6.txt"))
387         d.addCallback(lambda res:
388                       self.do_cli("get", "tahoe:subdir/uploaded6.txt"))
389         d.addCallback(lambda (stdout,stderr):
390                       self.failUnlessEqual(stdout, DATA))
391
392         return d
393
394     def test_mutable_unlinked(self):
395         # FILECAP = `echo DATA | tahoe put --mutable`
396         # tahoe get FILECAP, compare against DATA
397         # echo DATA2 | tahoe put - FILECAP
398         # tahoe get FILECAP, compare against DATA2
399         # tahoe put file.txt FILECAP
400         self.basedir = os.path.dirname(self.mktemp())
401         DATA = "data" * 100
402         DATA2 = "two" * 100
403         rel_fn = os.path.join(self.basedir, "DATAFILE")
404         abs_fn = os.path.abspath(rel_fn)
405         DATA3 = "three" * 100
406         f = open(rel_fn, "w")
407         f.write(DATA3)
408         f.close()
409
410         d = self.set_up_nodes()
411
412         d.addCallback(lambda res: self.do_cli("put", "--mutable", stdin=DATA))
413         def _created(res):
414             (stdout, stderr) = res
415             self.failUnless("waiting for file data on stdin.." in stderr)
416             self.failUnless("200 OK" in stderr)
417             self.filecap = stdout
418             self.failUnless(self.filecap.startswith("URI:SSK:"))
419         d.addCallback(_created)
420         d.addCallback(lambda res: self.do_cli("get", self.filecap))
421         d.addCallback(lambda (out,err): self.failUnlessEqual(out, DATA))
422
423         d.addCallback(lambda res: self.do_cli("put", "-", self.filecap, stdin=DATA2))
424         def _replaced(res):
425             (stdout, stderr) = res
426             self.failUnless("waiting for file data on stdin.." in stderr)
427             self.failUnless("200 OK" in stderr)
428             self.failUnlessEqual(self.filecap, stdout)
429         d.addCallback(_replaced)
430         d.addCallback(lambda res: self.do_cli("get", self.filecap))
431         d.addCallback(lambda (out,err): self.failUnlessEqual(out, DATA2))
432
433         d.addCallback(lambda res: self.do_cli("put", rel_fn, self.filecap))
434         def _replaced2(res):
435             (stdout, stderr) = res
436             self.failUnless("200 OK" in stderr)
437             self.failUnlessEqual(self.filecap, stdout)
438         d.addCallback(_replaced2)
439         d.addCallback(lambda res: self.do_cli("get", self.filecap))
440         d.addCallback(lambda (out,err): self.failUnlessEqual(out, DATA3))
441
442         return d
443
444     def test_mutable(self):
445         # echo DATA1 | tahoe put --mutable - uploaded.txt
446         # echo DATA2 | tahoe put - uploaded.txt # should modify-in-place
447         # tahoe get uploaded.txt, compare against DATA2
448
449         self.basedir = os.path.dirname(self.mktemp())
450         DATA1 = "data" * 100
451         fn1 = os.path.join(self.basedir, "DATA1")
452         f = open(fn1, "w")
453         f.write(DATA1)
454         f.close()
455         DATA2 = "two" * 100
456         fn2 = os.path.join(self.basedir, "DATA2")
457         f = open(fn2, "w")
458         f.write(DATA2)
459         f.close()
460
461         d = self.set_up_nodes()
462         d.addCallback(lambda res: self.do_cli("create-alias", "tahoe"))
463         d.addCallback(lambda res:
464                       self.do_cli("put", "--mutable", fn1, "tahoe:uploaded.txt"))
465         d.addCallback(lambda res:
466                       self.do_cli("put", fn2, "tahoe:uploaded.txt"))
467         d.addCallback(lambda res:
468                       self.do_cli("get", "tahoe:uploaded.txt"))
469         d.addCallback(lambda (out,err): self.failUnlessEqual(out, DATA2))
470         return d
471
472
473