universum 0.3.1 → 0.4.0
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.
- checksums.yaml +4 -4
- data/Manifest.txt +3 -0
- data/README.md +2 -2
- data/lib/universum.rb +1 -0
- data/lib/universum/account.rb +8 -7
- data/lib/universum/address.rb +26 -27
- data/lib/universum/contract.rb +51 -5
- data/lib/universum/receipt.rb +1 -1
- data/lib/universum/storage.rb +48 -0
- data/lib/universum/transaction.rb +3 -3
- data/lib/universum/universum.rb +7 -4
- data/lib/universum/version.rb +2 -2
- data/test/contracts/ballot.rb +1 -1
- data/test/contracts/greeter.rb +1 -1
- data/test/contracts/mytoken.rb +2 -2
- data/test/test_mytoken.rb +33 -0
- data/test/test_storage.rb +36 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ed79bd806a5db0f1d5a92a0dfd2e7f3f3b3db56
|
4
|
+
data.tar.gz: 89afee15e5c5800220c13f8f7b58dffb190a4177
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed45e6ab943f70fcd6a9c43c4faa98ab6f6e606f5808e188276b16efc1464603d393ccb4763484c4f33e8f0386a0644a83a6c0856b6197054240f67f486884bf
|
7
|
+
data.tar.gz: f9f0fe016eb2113b1705ec86aed96530690b250e8b83dbff0751b43b7d901ef404dd3bb769ef5e7ae66d75e9ec26cedca6e219fc6561c42e99355e17320be980
|
data/Manifest.txt
CHANGED
@@ -12,6 +12,7 @@ lib/universum/enum.rb
|
|
12
12
|
lib/universum/event.rb
|
13
13
|
lib/universum/function.rb
|
14
14
|
lib/universum/receipt.rb
|
15
|
+
lib/universum/storage.rb
|
15
16
|
lib/universum/transaction.rb
|
16
17
|
lib/universum/type.rb
|
17
18
|
lib/universum/units_money.rb
|
@@ -26,6 +27,8 @@ test/test_ballot.rb
|
|
26
27
|
test/test_enum.rb
|
27
28
|
test/test_event.rb
|
28
29
|
test/test_greeter.rb
|
30
|
+
test/test_mytoken.rb
|
31
|
+
test/test_storage.rb
|
29
32
|
test/test_units_money.rb
|
30
33
|
test/test_units_time.rb
|
31
34
|
test/test_version.rb
|
data/README.md
CHANGED
@@ -59,7 +59,7 @@ and
|
|
59
59
|
##################
|
60
60
|
# Greeter Contract
|
61
61
|
|
62
|
-
def
|
62
|
+
def setup( greeting )
|
63
63
|
@owner = msg.sender
|
64
64
|
@greeting = greeting
|
65
65
|
end
|
@@ -141,7 +141,7 @@ and
|
|
141
141
|
############################
|
142
142
|
## My Token Contract
|
143
143
|
|
144
|
-
def
|
144
|
+
def setup( initial_supply )
|
145
145
|
@balance_of = Mapping.of( Address => Money )
|
146
146
|
@balance_of[ msg.sender] = initial_supply
|
147
147
|
end
|
data/lib/universum.rb
CHANGED
data/lib/universum/account.rb
CHANGED
@@ -27,25 +27,26 @@ class Account
|
|
27
27
|
def self.all() @@directory.values; end
|
28
28
|
|
29
29
|
|
30
|
+
|
30
31
|
####
|
31
32
|
# account (builtin) services / transaction methods
|
32
|
-
include Address ## includes address + send/transfer/balance
|
33
33
|
|
34
|
+
def address() @address; end
|
35
|
+
def address_hex() @address.hex; end ## add convenience shortcut address_hex - why? why not?
|
36
|
+
|
37
|
+
def balance() @address.balance; end ## note: add convenience shortcut - why? why not?
|
34
38
|
## note: for now allow write access too!!!
|
35
39
|
def balance=( value )
|
36
|
-
@balance
|
40
|
+
@address.send( '@balance=', value )
|
37
41
|
end
|
38
42
|
|
43
|
+
|
39
44
|
attr_reader :tx
|
40
45
|
def _auto_inc_tx() @tx += 1; end ## "internal" method - (auto) increment transaction (tx) counter
|
41
46
|
|
42
|
-
## note: needed by transfer/send
|
43
|
-
def this() Universum.this; end ## returns current contract
|
44
|
-
|
45
47
|
private
|
46
48
|
def initialize( address, balance: 0, tx: 0 )
|
47
|
-
@address = address # type address - (hex) string starts with 0x
|
48
|
-
@balance = balance # uint
|
49
|
+
@address = Address.new( address, balance: balance ) # type address - (hex) string starts with 0x
|
49
50
|
@tx = tx # transaction (tx) count (used for nonce and replay attack protection)
|
50
51
|
end
|
51
52
|
|
data/lib/universum/address.rb
CHANGED
@@ -1,64 +1,61 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
class Address
|
5
|
+
def self.zero() '0x0000'; end
|
6
6
|
|
7
|
+
def initialize( address, balance: 0 )
|
8
|
+
@address = address
|
9
|
+
@balance = balance
|
10
|
+
end
|
11
|
+
|
12
|
+
def hex() @address; end ## return address as a hex string (e.g. '0x1111' etc.)
|
13
|
+
def balance() @balance; end
|
7
14
|
|
8
|
-
module Address
|
9
|
-
def self.zero() '0x0000'; end
|
10
15
|
|
11
|
-
def address
|
12
|
-
if @address
|
13
|
-
@address
|
14
|
-
else
|
15
|
-
if is_a? Contract
|
16
|
-
## fix/todo: use/lookup proper addr from contract
|
17
|
-
## construct address for now from object_id
|
18
|
-
"0x#{(object_id << 1).to_s(16)}"
|
19
|
-
else ## assume Account
|
20
|
-
'0x0000'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end # method address
|
24
16
|
|
25
17
|
def transfer( value ) ## @payable @public
|
26
18
|
## todo/fix: throw exception if insufficient funds
|
19
|
+
## todo/fix: use assert( send( value )
|
27
20
|
send( value ) # returns true/false
|
28
21
|
end
|
29
22
|
|
30
23
|
def send( value ) ## @payable @public
|
24
|
+
## note: auto-adds "global" from address (using Universum.this)
|
25
|
+
_send( Universum.this, value )
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
#############################
|
30
|
+
### private (internal use only) methods - PLEASE do NOT use (use transfer/send)
|
31
|
+
def _send( from, value )
|
31
32
|
## todo/fix: assert value > 0
|
32
33
|
## todo/fix: add missing -= part in transfer!!!!
|
33
34
|
|
34
35
|
## use this (current contract) for debit (-) ammount
|
35
|
-
|
36
|
+
from._sub( value ) # sub(tract) / debit from the sender (current contract)
|
36
37
|
_add( value ) # add / credit to the recipient
|
37
38
|
end
|
38
39
|
|
39
|
-
def balance
|
40
|
-
@balance ||= 0 ## return 0 if undefined
|
41
|
-
end
|
42
|
-
|
43
|
-
### private (internal use only) methods - PLEASE do NOT use (use transfer/send)
|
44
40
|
def _sub( value )
|
45
|
-
@balance ||= 0 ## return 0 if undefined
|
46
41
|
@balance -= value
|
47
42
|
end
|
48
43
|
|
49
44
|
def _add( value )
|
50
|
-
@balance ||= 0 ## return 0 if undefined
|
51
45
|
@balance += value
|
52
46
|
end
|
53
47
|
end # module Address
|
54
48
|
|
55
49
|
|
56
50
|
|
51
|
+
|
57
52
|
class String
|
58
53
|
def transfer( value )
|
59
54
|
## check if self is an address
|
60
55
|
if self.start_with?( '0x' )
|
61
|
-
|
56
|
+
## fix/fix/fix: use Address[self] lookup!!!!
|
57
|
+
## do NOT use Account any longer
|
58
|
+
Account[self].address.transfer( value )
|
62
59
|
else
|
63
60
|
raise "(Auto-)Type Conversion from Address (Hex) String to Account Failed; Expected String Starting with 0x got #{self}; Contract Halted (Stopped)"
|
64
61
|
end
|
@@ -67,7 +64,9 @@ class String
|
|
67
64
|
def send( value )
|
68
65
|
## check if self is an address
|
69
66
|
if self.start_with?( '0x' )
|
70
|
-
|
67
|
+
## fix/fix/fix: use Address[self] lookup!!!!
|
68
|
+
## do NOT use Account any longer
|
69
|
+
Account[self].address.send( value )
|
71
70
|
else
|
72
71
|
raise "(Auto-)Type Conversion from Address (Hex) String to Account Failed; Expected String Starting with 0x got #{self}; Contract Halted (Stopped)"
|
73
72
|
end
|
data/lib/universum/contract.rb
CHANGED
@@ -17,6 +17,25 @@ class Contract
|
|
17
17
|
end
|
18
18
|
|
19
19
|
code = File.open( path, 'r:bom|utf-8' ) { |f| f.read }
|
20
|
+
|
21
|
+
## auto-patch!!!!
|
22
|
+
## change all ivars to use storage
|
23
|
+
## e.g. @greeting => storage.greeting
|
24
|
+
## @balance_of[ msg.sender ] => storage.balance_of[ msg.sender ]
|
25
|
+
## ...
|
26
|
+
##
|
27
|
+
## todo/fix: check for possible class variables!! (e.g. @@logger or something)
|
28
|
+
## make regex "smarter"
|
29
|
+
code = code.gsub( /(@{1,})([a-z][a-zA-Z0-9_]*)/ ) do |_|
|
30
|
+
if $1.size == 1
|
31
|
+
puts "auto-patching contract code >#{$1}#{$2}< to >storage[ :#{$2} ]<"
|
32
|
+
"storage[ :#{$2} ]"
|
33
|
+
else
|
34
|
+
## assume class variable - skip - keep as is
|
35
|
+
"#{$1}#{$2}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
20
39
|
klass = Class.new( Contract )
|
21
40
|
klass.class_eval( code ) ## note: use class_eval (NOT instance_eval)
|
22
41
|
klass
|
@@ -40,7 +59,35 @@ class Contract
|
|
40
59
|
|
41
60
|
####
|
42
61
|
# account (builtin) services / transaction methods
|
43
|
-
|
62
|
+
def self.create( *args )
|
63
|
+
klass = new( nil, *args )
|
64
|
+
klass.setup( *args )
|
65
|
+
klass
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize( address=nil, *args )
|
69
|
+
## fix/todo: use/lookup proper addr from contract
|
70
|
+
## construct address for now from object_id
|
71
|
+
address = "0x#{(object_id << 1).to_s(16)}" if address.nil?
|
72
|
+
@address = Address.new( address )
|
73
|
+
@storage = Storage.new
|
74
|
+
|
75
|
+
###########
|
76
|
+
# note: does NOT auto-call setup - why? why not?
|
77
|
+
# use create !!!!!!
|
78
|
+
|
79
|
+
## todo: make initialize private - why? why not?
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def setup
|
84
|
+
# default (built-in) setup; do nothing
|
85
|
+
end
|
86
|
+
|
87
|
+
def address() @address; end
|
88
|
+
def this() @address; end ## returns "embedded" address of current contract (that is, us!)
|
89
|
+
|
90
|
+
def storage() @storage; end
|
44
91
|
|
45
92
|
## function sig(nature) macro for types (dummy for now)
|
46
93
|
# e.g. use like
|
@@ -54,9 +101,9 @@ class Contract
|
|
54
101
|
|
55
102
|
####
|
56
103
|
# todo/double check: auto-add payable default fallback - why? why not?
|
57
|
-
|
58
|
-
|
59
|
-
|
104
|
+
# no - do NOT add, BUT add payable config for receive
|
105
|
+
# def receive ## @payable default fallback - use different name - why? why not? (e.g. handle/process/etc.)
|
106
|
+
# end
|
60
107
|
|
61
108
|
def assert( condition )
|
62
109
|
if condition == true
|
@@ -67,7 +114,6 @@ class Contract
|
|
67
114
|
end
|
68
115
|
|
69
116
|
|
70
|
-
def this() Universum.this; end ## returns current contract
|
71
117
|
def log( event ) Universum.log( event ); end
|
72
118
|
def msg() Universum.msg; end
|
73
119
|
def block() Universum.block; end
|
data/lib/universum/receipt.rb
CHANGED
@@ -36,7 +36,7 @@ class Receipt ## transaction receipt
|
|
36
36
|
|
37
37
|
if contract
|
38
38
|
## note: for easier debugging add class name in () to address (needs to get stripped away in lookup)
|
39
|
-
@contract_address = "#{contract.address} (#{contract.class.name})"
|
39
|
+
@contract_address = "#{contract.address.hex} (#{contract.class.name})"
|
40
40
|
else
|
41
41
|
@contract_address = nil
|
42
42
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
#######
|
4
|
+
# keep track of contract state
|
5
|
+
|
6
|
+
class Storage
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@data = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
puts " SLOAD( #{key.inspect} )"
|
14
|
+
@data[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(key,value)
|
18
|
+
puts " SSTORE( #{key.inspect}, #{value.inspect} )"
|
19
|
+
@data[key] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
###############################
|
25
|
+
# todo/future: in the future auto-add getter/setter methods on setup
|
26
|
+
# do NOT use method_missing!!!
|
27
|
+
def method_missing( m, *args, &block )
|
28
|
+
puts "Storage#method_missing( #{m.inspect}), #{args.inspect} )"
|
29
|
+
|
30
|
+
## todo: add support ? for bool
|
31
|
+
## elsif m.end_with?('?') && args.empty?
|
32
|
+
## @storage[m]
|
33
|
+
|
34
|
+
if m =~/\A[a-z][a-zA-Z0-9_]*=\z/ && args.size == 1
|
35
|
+
key = m[0...-1].to_sym ## note: cut-off trailing equal sign (=), use EXCLUSIVE (...) range and NOT INCLUSIVE (..)
|
36
|
+
value = args[0]
|
37
|
+
puts " SSTORE( #{key.inspect}, #{value.inspect} )"
|
38
|
+
@data[key] = value
|
39
|
+
elsif m =~/\A[a-z][a-zA-Z0-9_]*\z/ && args.empty? ## todo/fix: check for valid identifier
|
40
|
+
key = m
|
41
|
+
puts " SLOAD( #{key.inspect} )"
|
42
|
+
@data[key]
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end ## class Storage
|
@@ -18,12 +18,12 @@ class Transaction
|
|
18
18
|
account = Account.at( from ) ## lookup account by address
|
19
19
|
end
|
20
20
|
|
21
|
-
@from = account.address
|
21
|
+
@from = account.address.hex
|
22
22
|
|
23
23
|
if to.is_a?( Contract )
|
24
|
-
@to = "#{to.address} (#{to.class.name})"
|
24
|
+
@to = "#{to.address.hex} (#{to.class.name})"
|
25
25
|
elsif to.is_a?( Account ) ## note: to allows Contracts AND Accounts
|
26
|
-
@to = to.address
|
26
|
+
@to = to.address.hex
|
27
27
|
else
|
28
28
|
@to = to # might be a contract or account (pass through for now)
|
29
29
|
end
|
data/lib/universum/universum.rb
CHANGED
@@ -31,6 +31,9 @@ class Universum ## Uni short for Universum
|
|
31
31
|
counter = @@counter ||= 0 ## total tx counter for debugging (start with 0)
|
32
32
|
@@counter += 1
|
33
33
|
|
34
|
+
## todo/fix:
|
35
|
+
## allow/add auto-create account for convenience (and easy testing)!!!
|
36
|
+
|
34
37
|
## note: always lookup (use) account for now
|
35
38
|
if from.is_a?( Account )
|
36
39
|
account = from
|
@@ -39,7 +42,7 @@ class Universum ## Uni short for Universum
|
|
39
42
|
end
|
40
43
|
|
41
44
|
## setup contract msg context
|
42
|
-
self.msg = { sender: account.address, value: value }
|
45
|
+
self.msg = { sender: account.address.hex, value: value }
|
43
46
|
|
44
47
|
|
45
48
|
## allow shortcut for Class (without ctor arguments) - no need to wrap in array
|
@@ -54,7 +57,7 @@ class Universum ## Uni short for Universum
|
|
54
57
|
args = data[1..-1] ## arguments
|
55
58
|
|
56
59
|
puts "** tx ##{counter} (block ##{block.number}): #{tx.log_str}"
|
57
|
-
contract = klass.
|
60
|
+
contract = klass.create( *args ) ## note: balance and this (and msg.send/transfer) NOT available/possible !!!!
|
58
61
|
|
59
62
|
if value > 0
|
60
63
|
## move value to msg (todo/fix: restore if exception)
|
@@ -62,9 +65,9 @@ class Universum ## Uni short for Universum
|
|
62
65
|
contract._add( value ) # add / credit to the recipient
|
63
66
|
end
|
64
67
|
|
65
|
-
puts " new #{contract.class.name} contract adddress: #{contract.address.inspect}"
|
68
|
+
puts " new #{contract.class.name} contract adddress: #{contract.address.hex.inspect}"
|
66
69
|
## add new contract to (lookup) directory
|
67
|
-
Contract.store( contract.address, contract )
|
70
|
+
Contract.store( contract.address.hex, contract )
|
68
71
|
|
69
72
|
## issue (mined) transaction receipt
|
70
73
|
receipt = Receipt.new( tx: tx,
|
data/lib/universum/version.rb
CHANGED
data/test/contracts/ballot.rb
CHANGED
@@ -9,7 +9,7 @@ Voter = Struct.new( weight: 0,
|
|
9
9
|
Proposal = Struct.new( vote_count: 0 )
|
10
10
|
|
11
11
|
## Create a new ballot with $(num_proposals) different proposals.
|
12
|
-
def
|
12
|
+
def setup( num_proposals )
|
13
13
|
@chairperson = msg.sender
|
14
14
|
@voters = Mapping.of( Address => Voter )
|
15
15
|
@proposals = Array.of( Proposal, num_proposals )
|
data/test/contracts/greeter.rb
CHANGED
data/test/contracts/mytoken.rb
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
#########################
|
4
4
|
# My Token Contract
|
5
5
|
|
6
|
-
def
|
6
|
+
def setup( initial_supply )
|
7
7
|
@balance_of = Mapping.of( Address => Money )
|
8
|
-
@balance_of[ msg.sender] = initial_supply
|
8
|
+
@balance_of[ msg.sender ] = initial_supply
|
9
9
|
end
|
10
10
|
|
11
11
|
def transfer( to, value )
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
###
|
4
|
+
# to run use
|
5
|
+
# ruby -I ./lib -I ./test test/test_mytoken.rb
|
6
|
+
|
7
|
+
|
8
|
+
require 'helper'
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
class TestMyToken < MiniTest::Test
|
13
|
+
|
14
|
+
MyToken = Contract.load( "#{Universum.root}/test/contracts/mytoken" )
|
15
|
+
|
16
|
+
def test_uni
|
17
|
+
Account['0x1111']
|
18
|
+
Account['0xaaaa']
|
19
|
+
Account['0xbbbb']
|
20
|
+
|
21
|
+
tx = Uni.send_transaction( from: '0x1111', data: [MyToken, 100_000_000] )
|
22
|
+
pp tx
|
23
|
+
pp tx.receipt
|
24
|
+
|
25
|
+
token = tx.receipt.contract
|
26
|
+
|
27
|
+
Uni.send_transaction( from: '0x1111', to: token, data: [:transfer, '0xaaaa', 100] )
|
28
|
+
Uni.send_transaction( from: '0x1111', to: token, data: [:transfer, '0xbbbb', 200] )
|
29
|
+
Uni.send_transaction( from: '0xbbbb', to: token, data: [:transfer, '0xaaaa', 10] )
|
30
|
+
pp token
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class TestMyToken
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
###
|
4
|
+
# to run use
|
5
|
+
# ruby -I ./lib -I ./test test/test_storage.rb
|
6
|
+
|
7
|
+
|
8
|
+
require 'helper'
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
class TestStorage < MiniTest::Test
|
14
|
+
|
15
|
+
def test_storage
|
16
|
+
|
17
|
+
s = Storage.new
|
18
|
+
s[:balance]
|
19
|
+
s[:balance] = {}
|
20
|
+
s[:balance]['0xaaaa'] = 100
|
21
|
+
s[:balance]['0xbbbb'] = 200
|
22
|
+
pp s
|
23
|
+
|
24
|
+
balance_exp = { '0xaaaa'=>100, '0xbbbb'=>200 }
|
25
|
+
assert_equal balance_exp, s.balance
|
26
|
+
assert_equal 100, s.balance['0xaaaa']
|
27
|
+
assert_equal 200, s.balance['0xbbbb']
|
28
|
+
|
29
|
+
s.balance['0xcccc'] = 300
|
30
|
+
assert_equal 300, s.balance['0xcccc']
|
31
|
+
|
32
|
+
s.owner = '0x1111'
|
33
|
+
assert_equal '0x1111', s.owner
|
34
|
+
end
|
35
|
+
|
36
|
+
end # class TestStorage
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: universum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-02-
|
11
|
+
date: 2019-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: safestruct
|
@@ -79,6 +79,7 @@ files:
|
|
79
79
|
- lib/universum/event.rb
|
80
80
|
- lib/universum/function.rb
|
81
81
|
- lib/universum/receipt.rb
|
82
|
+
- lib/universum/storage.rb
|
82
83
|
- lib/universum/transaction.rb
|
83
84
|
- lib/universum/type.rb
|
84
85
|
- lib/universum/units_money.rb
|
@@ -93,6 +94,8 @@ files:
|
|
93
94
|
- test/test_enum.rb
|
94
95
|
- test/test_event.rb
|
95
96
|
- test/test_greeter.rb
|
97
|
+
- test/test_mytoken.rb
|
98
|
+
- test/test_storage.rb
|
96
99
|
- test/test_units_money.rb
|
97
100
|
- test/test_units_time.rb
|
98
101
|
- test/test_version.rb
|