mogilefs-client 2.2.0 → 3.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +11 -0
- data/.gemtest +0 -0
- data/.gitignore +4 -0
- data/.wrongdoc.yml +5 -0
- data/GIT-VERSION-GEN +28 -0
- data/GNUmakefile +44 -0
- data/HACKING +33 -0
- data/{History.txt → History} +0 -1
- data/{LICENSE.txt → LICENSE} +0 -1
- data/Manifest.txt +34 -7
- data/README +51 -0
- data/Rakefile +11 -11
- data/TODO +10 -0
- data/bin/mog +109 -68
- data/examples/mogstored_rack.rb +189 -0
- data/lib/mogilefs.rb +56 -17
- data/lib/mogilefs/admin.rb +128 -62
- data/lib/mogilefs/backend.rb +205 -95
- data/lib/mogilefs/bigfile.rb +54 -70
- data/lib/mogilefs/bigfile/filter.rb +58 -0
- data/lib/mogilefs/chunker.rb +30 -0
- data/lib/mogilefs/client.rb +0 -2
- data/lib/mogilefs/copy_stream.rb +30 -0
- data/lib/mogilefs/http_file.rb +175 -0
- data/lib/mogilefs/http_reader.rb +79 -0
- data/lib/mogilefs/mogilefs.rb +242 -148
- data/lib/mogilefs/mysql.rb +3 -4
- data/lib/mogilefs/paths_size.rb +24 -0
- data/lib/mogilefs/pool.rb +0 -1
- data/lib/mogilefs/socket.rb +9 -0
- data/lib/mogilefs/socket/kgio.rb +55 -0
- data/lib/mogilefs/socket/pure_ruby.rb +70 -0
- data/lib/mogilefs/socket_common.rb +58 -0
- data/lib/mogilefs/util.rb +6 -169
- data/test/aggregate.rb +11 -11
- data/test/exec.rb +72 -0
- data/test/fresh.rb +222 -0
- data/test/integration.rb +43 -0
- data/test/setup.rb +1 -0
- data/test/socket_test.rb +98 -0
- data/test/test_admin.rb +14 -37
- data/test/test_backend.rb +50 -107
- data/test/test_bigfile.rb +2 -2
- data/test/test_db_backend.rb +1 -2
- data/test/test_fresh.rb +8 -0
- data/test/test_http_reader.rb +34 -0
- data/test/test_mogilefs.rb +278 -98
- data/test/test_mogilefs_integration.rb +174 -0
- data/test/test_mogilefs_integration_large_pipe.rb +62 -0
- data/test/test_mogilefs_integration_list_keys.rb +40 -0
- data/test/test_mogilefs_socket_kgio.rb +11 -0
- data/test/test_mogilefs_socket_pure.rb +7 -0
- data/test/test_mogstored_rack.rb +89 -0
- data/test/test_mogtool_bigfile.rb +116 -0
- data/test/test_mysql.rb +1 -2
- data/test/test_pool.rb +1 -1
- data/test/test_unit_mogstored_rack.rb +72 -0
- metadata +76 -54
- data/README.txt +0 -80
- data/lib/mogilefs/httpfile.rb +0 -157
- data/lib/mogilefs/network.rb +0 -107
- data/test/test_network.rb +0 -56
- data/test/test_util.rb +0 -121
data/lib/mogilefs/mysql.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
# Consider this deprecated, to be removed at some point...
|
3
|
+
#
|
5
4
|
# read-only interface that can be a backend for MogileFS::MogileFS
|
6
5
|
#
|
7
6
|
# This provides direct, read-only access to any slave MySQL database to
|
@@ -29,7 +28,7 @@ class MogileFS::Mysql
|
|
29
28
|
##
|
30
29
|
# Lists keys starting with +prefix+ follwing +after+ up to +limit+. If
|
31
30
|
# +after+ is nil the list starts at the beginning.
|
32
|
-
def _list_keys(domain, prefix = '', after = '', limit = 1000
|
31
|
+
def _list_keys(domain, prefix = '', after = '', limit = 1000)
|
33
32
|
# this code is based on server/lib/MogileFS/Worker/Query.pm
|
34
33
|
dmid = get_dmid(domain)
|
35
34
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# This is only a hack for old MogileFS installs that didn't have file_info
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
module MogileFS::PathsSize
|
6
|
+
def self.call(paths)
|
7
|
+
errors = {}
|
8
|
+
paths.each do |path|
|
9
|
+
uri = URI.parse(path)
|
10
|
+
begin
|
11
|
+
case r = Net::HTTP.start(uri.host, uri.port) { |x| x.head(uri.path) }
|
12
|
+
when Net::HTTPOK
|
13
|
+
return r["Content-Length"].to_i
|
14
|
+
else
|
15
|
+
errors[path] = r
|
16
|
+
end
|
17
|
+
rescue => err
|
18
|
+
errors[path] = err
|
19
|
+
end
|
20
|
+
end
|
21
|
+
errors = errors.map { |path,err| "#{path} - #{err.message} (#{err.class})" }
|
22
|
+
raise MogileFS::Error, "all paths failed with HEAD: #{errors.join(', ')}"
|
23
|
+
end
|
24
|
+
end
|
data/lib/mogilefs/pool.rb
CHANGED
@@ -0,0 +1,9 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# internal implementation details here, do not rely on this in your code
|
3
|
+
require "mogilefs/socket_common"
|
4
|
+
begin
|
5
|
+
raise LoadError, "testing pure Ruby version" if ENV["MOGILEFS_CLIENT_PURE"]
|
6
|
+
require "mogilefs/socket/kgio"
|
7
|
+
rescue LoadError
|
8
|
+
require "mogilefs/socket/pure_ruby"
|
9
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# internal implementation details here, do not rely on them in your code
|
3
|
+
require "kgio"
|
4
|
+
|
5
|
+
class MogileFS::Socket < Kgio::Socket
|
6
|
+
include MogileFS::SocketCommon
|
7
|
+
|
8
|
+
def self.start(host, port)
|
9
|
+
sock = super(Socket.sockaddr_in(port, host))
|
10
|
+
sock.post_init(host, port)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.tcp(host, port, timeout = 5)
|
14
|
+
sock = start(host, port)
|
15
|
+
unless sock.kgio_wait_writable(timeout)
|
16
|
+
sock.close
|
17
|
+
raise MogileFS::Timeout, 'socket connect timeout'
|
18
|
+
end
|
19
|
+
sock
|
20
|
+
end
|
21
|
+
|
22
|
+
def timed_read(len, dst = "", timeout = 5)
|
23
|
+
case rc = kgio_tryread(len, dst)
|
24
|
+
when :wait_readable
|
25
|
+
kgio_wait_readable(timeout) or unreadable_socket!
|
26
|
+
else
|
27
|
+
return rc
|
28
|
+
end while true
|
29
|
+
end
|
30
|
+
|
31
|
+
def timed_peek(len, dst, timeout = 5)
|
32
|
+
case rc = kgio_trypeek(len, dst)
|
33
|
+
when :wait_readable
|
34
|
+
kgio_wait_readable(timeout) or unreadable_socket!
|
35
|
+
else
|
36
|
+
return rc
|
37
|
+
end while true
|
38
|
+
end
|
39
|
+
|
40
|
+
def timed_write(buf, timeout = 5)
|
41
|
+
written = 0
|
42
|
+
expect = buf.bytesize
|
43
|
+
case rc = kgio_trywrite(buf)
|
44
|
+
when :wait_writable
|
45
|
+
kgio_wait_writable(timeout) or request_truncated!(written, expect)
|
46
|
+
when String
|
47
|
+
written += expect - rc.bytesize
|
48
|
+
buf = rc
|
49
|
+
else
|
50
|
+
return expect
|
51
|
+
end while true
|
52
|
+
end
|
53
|
+
|
54
|
+
alias write timed_write
|
55
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# internal implementation details here, do not rely on them in your code
|
3
|
+
|
4
|
+
class MogileFS::Socket < Socket
|
5
|
+
include MogileFS::SocketCommon
|
6
|
+
|
7
|
+
def self.start(host, port)
|
8
|
+
sock = new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
9
|
+
begin
|
10
|
+
sock.connect_nonblock(sockaddr_in(port, host))
|
11
|
+
rescue Errno::EINPROGRESS
|
12
|
+
end
|
13
|
+
sock.post_init(host, port)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.tcp(host, port, timeout = 5)
|
17
|
+
sock = start(host, port)
|
18
|
+
unless IO.select(nil, [ sock ], nil, timeout)
|
19
|
+
sock.close
|
20
|
+
raise MogileFS::Timeout, 'socket connect timeout'
|
21
|
+
end
|
22
|
+
sock
|
23
|
+
end
|
24
|
+
|
25
|
+
def timed_read(len, dst = "", timeout = 5)
|
26
|
+
begin
|
27
|
+
IO.select([self], nil, nil, timeout) or unreadable_socket!
|
28
|
+
return read_nonblock(len, dst)
|
29
|
+
rescue Errno::EAGAIN
|
30
|
+
rescue EOFError
|
31
|
+
return
|
32
|
+
end while true
|
33
|
+
end
|
34
|
+
|
35
|
+
def timed_peek(len, dst, timeout = 5)
|
36
|
+
begin
|
37
|
+
IO.select([self], nil, nil, timeout) or unreadable_socket!
|
38
|
+
rc = recv_nonblock(len, Socket::MSG_PEEK)
|
39
|
+
return rc.empty? ? nil : dst.replace(rc)
|
40
|
+
rescue Errno::EAGAIN
|
41
|
+
rescue EOFError
|
42
|
+
dst.replace("")
|
43
|
+
return
|
44
|
+
end while true
|
45
|
+
end
|
46
|
+
|
47
|
+
def timed_write(buf, timeout = 5)
|
48
|
+
written = 0
|
49
|
+
expect = buf.bytesize
|
50
|
+
begin
|
51
|
+
rc = write_nonblock(buf)
|
52
|
+
return expect if rc == buf.bytesize
|
53
|
+
written += rc
|
54
|
+
|
55
|
+
if buf.respond_to?(:byteslice)
|
56
|
+
buf = buf.byteslice(rc, buf.bytesize)
|
57
|
+
else
|
58
|
+
if buf.respond_to?(:encoding) && buf.encoding != Encoding::BINARY
|
59
|
+
buf = buf.dup.force_encoding(Encoding::BINARY)
|
60
|
+
end
|
61
|
+
buf = buf.slice(rc, buf.bytesize)
|
62
|
+
end
|
63
|
+
rescue Errno::EAGAIN
|
64
|
+
IO.select(nil, [self], nil, timeout) or
|
65
|
+
request_truncated!(written, expect)
|
66
|
+
end while true
|
67
|
+
end
|
68
|
+
|
69
|
+
alias write timed_write
|
70
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# internal implementation details here, do not rely on this in your code
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module MogileFS::SocketCommon
|
6
|
+
attr_reader :mogilefs_addr
|
7
|
+
|
8
|
+
def post_init(host, port)
|
9
|
+
@mogilefs_addr = "#{host}:#{port}"
|
10
|
+
Socket.const_defined?(:TCP_NODELAY) and
|
11
|
+
setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def unreadable_socket!
|
16
|
+
raise MogileFS::UnreadableSocketError,
|
17
|
+
"#@mogilefs_addr never became readable"
|
18
|
+
end
|
19
|
+
|
20
|
+
def request_truncated!(written, expect)
|
21
|
+
raise MogileFS::RequestTruncatedError,
|
22
|
+
"request truncated (sent #{written} expected #{expect})"
|
23
|
+
end
|
24
|
+
|
25
|
+
SEP_RE = /\A(.*?#{Regexp.escape("\n")})/
|
26
|
+
def timed_gets(timeout = 5)
|
27
|
+
unless defined?(@rbuf) && @rbuf
|
28
|
+
@rbuf = timed_read(1024, "", timeout) or return # EOF
|
29
|
+
end
|
30
|
+
begin
|
31
|
+
@rbuf.sub!(SEP_RE, "") and return $1
|
32
|
+
tmp ||= ""
|
33
|
+
if timed_read(1024, tmp, timeout)
|
34
|
+
@rbuf << tmp
|
35
|
+
else
|
36
|
+
# EOF, return the last buffered bit even without SEP_RE matching
|
37
|
+
# (not ideal for MogileFS, this is an error)
|
38
|
+
return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size)
|
39
|
+
end
|
40
|
+
end while true
|
41
|
+
end
|
42
|
+
|
43
|
+
def read(size, buf = "", timeout = 5)
|
44
|
+
timed_read(size, buf, timeout) or return # nil/EOF
|
45
|
+
|
46
|
+
while size > buf.bytesize
|
47
|
+
tmp ||= ""
|
48
|
+
timed_read(size - buf.bytesize, tmp, timeout) or return buf # truncated
|
49
|
+
buf << tmp
|
50
|
+
end
|
51
|
+
|
52
|
+
buf # full read
|
53
|
+
end
|
54
|
+
|
55
|
+
def readpartial(size, buf = "", timeout = 5)
|
56
|
+
timed_read(size, buf, timeout) or raise EOFError, "end of file reached"
|
57
|
+
end
|
58
|
+
end
|
data/lib/mogilefs/util.rb
CHANGED
@@ -1,96 +1,13 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
require 'mogilefs'
|
3
|
-
require 'socket'
|
4
2
|
|
5
3
|
module MogileFS::Util
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# string or nil). This can be used to filter I/O through an
|
14
|
-
# Zlib::Inflate or Digest::MD5 object
|
15
|
-
def sysrwloop(io_rd, io_wr, filter = nil)
|
16
|
-
copied = 0
|
17
|
-
# avoid making sysread repeatedly allocate a new String
|
18
|
-
# This is not well-documented, but both read/sysread can take
|
19
|
-
# an optional second argument to use as the buffer to avoid
|
20
|
-
# GC overhead of creating new strings in a loop
|
21
|
-
buf = ' ' * CHUNK_SIZE # preallocate to avoid GC thrashing
|
22
|
-
io_rd.flush rescue nil # flush may be needed for sockets/pipes, be safe
|
23
|
-
io_wr.flush
|
24
|
-
io_rd.sync = io_wr.sync = true
|
25
|
-
loop do
|
26
|
-
b = begin
|
27
|
-
io_rd.sysread(CHUNK_SIZE, buf)
|
28
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
29
|
-
IO.select([io_rd], nil, nil, nil)
|
30
|
-
retry
|
31
|
-
rescue EOFError
|
32
|
-
break
|
33
|
-
end
|
34
|
-
b = filter.call(b) if filter
|
35
|
-
copied += syswrite_full(io_wr, b)
|
36
|
-
end
|
37
|
-
|
38
|
-
# filter must take nil as a possible argument to indicate EOF
|
39
|
-
if filter
|
40
|
-
b = filter.call(nil)
|
41
|
-
copied += syswrite_full(io_wr, b) if b && b.length > 0
|
42
|
-
end
|
43
|
-
copied
|
44
|
-
end # sysrwloop
|
45
|
-
|
46
|
-
# writes the contents of buf to io_wr in full w/o blocking
|
47
|
-
def syswrite_full(io_wr, buf, timeout = nil)
|
48
|
-
written = 0
|
49
|
-
loop do
|
50
|
-
begin
|
51
|
-
w = io_wr.syswrite(buf)
|
52
|
-
written += w
|
53
|
-
return written if w == buf.size
|
54
|
-
buf = buf[w..-1]
|
55
|
-
|
56
|
-
# a short syswrite means the next syswrite will likely block
|
57
|
-
# inside the interpreter. so force an IO.select on it so we can
|
58
|
-
# timeout there if one was specified
|
59
|
-
raise Errno::EAGAIN if timeout
|
60
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
61
|
-
t0 = Time.now if timeout
|
62
|
-
IO.select(nil, [io_wr], nil, timeout)
|
63
|
-
if timeout && ((timeout -= (Time.now - t0)) < 0)
|
64
|
-
raise MogileFS::Timeout, 'syswrite_full timeout'
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
# should never get here
|
69
|
-
end
|
70
|
-
|
71
|
-
def sysread_full(io_rd, size, timeout = nil, full_timeout = false)
|
72
|
-
tmp = [] # avoid expensive string concatenation with every loop iteration
|
73
|
-
reader = io_rd.method(timeout ? :read_nonblock : :sysread)
|
74
|
-
begin
|
75
|
-
while size > 0
|
76
|
-
tmp << reader.call(size)
|
77
|
-
size -= tmp.last.size
|
78
|
-
end
|
79
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
80
|
-
t0 = Time.now
|
81
|
-
r = IO.select([ io_rd ], nil, nil, timeout)
|
82
|
-
if timeout
|
83
|
-
timeout -= (Time.now - t0) if full_timeout
|
84
|
-
if !(r && r[0]) || timeout < 0
|
85
|
-
raise MogileFS::Timeout, 'sysread_full timeout'
|
86
|
-
end
|
87
|
-
end
|
88
|
-
retry
|
89
|
-
rescue EOFError
|
90
|
-
end
|
91
|
-
tmp.join('')
|
92
|
-
end
|
93
|
-
|
5
|
+
# MogileFS::Util::StoreContent allows you to roll your own method
|
6
|
+
# of streaming data on an upload (instead of using a string or file)
|
7
|
+
#
|
8
|
+
# Current versions of this library support streaming a IO or IO-like
|
9
|
+
# object to using MogileFS::MogileFS#store_file, so using StoreContent
|
10
|
+
# may no longer be necessary.
|
94
11
|
class StoreContent < Proc
|
95
12
|
def initialize(total_size, &writer_proc)
|
96
13
|
@total_size = total_size
|
@@ -100,7 +17,6 @@ module MogileFS::Util
|
|
100
17
|
@total_size
|
101
18
|
end
|
102
19
|
end
|
103
|
-
|
104
20
|
end
|
105
21
|
|
106
22
|
require 'timeout'
|
@@ -109,82 +25,3 @@ require 'timeout'
|
|
109
25
|
# reason we require the 'timeout' module, otherwise that module is
|
110
26
|
# broken and worthless to us.
|
111
27
|
class MogileFS::Timeout < Timeout::Error; end
|
112
|
-
|
113
|
-
class Socket
|
114
|
-
attr_accessor :mogilefs_addr, :mogilefs_connected, :mogilefs_size
|
115
|
-
|
116
|
-
# Socket lacks peeraddr method of the IPSocket/TCPSocket classes
|
117
|
-
def mogilefs_peername
|
118
|
-
Socket.unpack_sockaddr_in(getpeername).reverse.map {|x| x.to_s }.join(':')
|
119
|
-
end
|
120
|
-
|
121
|
-
def mogilefs_init(host = nil, port = nil)
|
122
|
-
return true if defined?(@mogilefs_connected)
|
123
|
-
|
124
|
-
@mogilefs_addr = Socket.sockaddr_in(port, host).freeze if port && host
|
125
|
-
|
126
|
-
begin
|
127
|
-
connect_nonblock(@mogilefs_addr)
|
128
|
-
@mogilefs_connected = true
|
129
|
-
rescue Errno::EINPROGRESS
|
130
|
-
nil
|
131
|
-
rescue Errno::EISCONN
|
132
|
-
@mogilefs_connected = true
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
class << self
|
137
|
-
|
138
|
-
# Creates a new (TCP) Socket and initiates (but does not wait for) the
|
139
|
-
# connection
|
140
|
-
def mogilefs_new_nonblock(host, port)
|
141
|
-
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
142
|
-
if defined?(Socket::TCP_NODELAY)
|
143
|
-
sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
144
|
-
end
|
145
|
-
sock.mogilefs_init(host, port)
|
146
|
-
sock
|
147
|
-
end
|
148
|
-
|
149
|
-
# Like TCPSocket.new(host, port), but with an explicit timeout
|
150
|
-
# (and we don't care for local address/port we're binding to).
|
151
|
-
# This raises MogileFS::Timeout if timeout expires
|
152
|
-
def mogilefs_new(host, port, timeout = 5.0)
|
153
|
-
sock = mogilefs_new_nonblock(host, port) or return sock
|
154
|
-
|
155
|
-
while timeout > 0
|
156
|
-
t0 = Time.now
|
157
|
-
r = IO.select(nil, [sock], nil, timeout)
|
158
|
-
return sock if r && r[1] && sock.mogilefs_init
|
159
|
-
timeout -= (Time.now - t0)
|
160
|
-
end
|
161
|
-
|
162
|
-
sock.close rescue nil
|
163
|
-
raise MogileFS::Timeout, 'socket write timeout'
|
164
|
-
end
|
165
|
-
|
166
|
-
include MogileFS::Util
|
167
|
-
|
168
|
-
# Makes a request on a new TCP Socket and returns with a readble socket
|
169
|
-
# within the given timeout.
|
170
|
-
# This raises MogileFS::Timeout if timeout expires
|
171
|
-
def mogilefs_new_request(host, port, request, timeout = 5.0)
|
172
|
-
t0 = Time.now
|
173
|
-
sock = mogilefs_new(host, port, timeout)
|
174
|
-
syswrite_full(sock, request, timeout)
|
175
|
-
timeout -= (Time.now - t0)
|
176
|
-
if timeout < 0
|
177
|
-
sock.close rescue nil
|
178
|
-
raise MogileFS::Timeout, 'socket read timeout'
|
179
|
-
end
|
180
|
-
r = IO.select([sock], nil, nil, timeout)
|
181
|
-
return sock if r && r[0]
|
182
|
-
|
183
|
-
sock.close rescue nil
|
184
|
-
raise MogileFS::Timeout, 'socket read timeout'
|
185
|
-
end
|
186
|
-
|
187
|
-
end
|
188
|
-
|
189
|
-
end
|
190
|
-
|
data/test/aggregate.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
#!/usr/bin/ruby
|
1
|
+
#!/usr/bin/ruby
|
2
2
|
# -*- encoding: binary -*-
|
3
|
-
|
3
|
+
$tests = $assertions = $failures = $errors = 0
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
$
|
8
|
-
$
|
9
|
-
$
|
5
|
+
STDIN.each_line do |l|
|
6
|
+
l =~ /(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors/ or next
|
7
|
+
$tests += $1.to_i
|
8
|
+
$assertions += $2.to_i
|
9
|
+
$failures += $3.to_i
|
10
|
+
$errors += $4.to_i
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
$tests, $assertions, $failures, $errors)
|
14
|
-
}
|
13
|
+
printf("\n%d tests, %d assertions, %d failures, %d errors\n",
|
14
|
+
$tests, $assertions, $failures, $errors)
|