summaryrefslogtreecommitdiff
path: root/test/ruby/test_random_formatter.rb
blob: f927522d96864d2514ea6c23b5d8587cddbe38e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
require 'test/unit'
require 'random/formatter'

module Random::Formatter
  module FormatterTest
    def test_random_bytes
      assert_equal(16, @it.random_bytes.size)
      assert_equal(Encoding::ASCII_8BIT, @it.random_bytes.encoding)
      65.times do |idx|
        assert_equal(idx, @it.random_bytes(idx).size)
      end
    end

    def test_hex
      s = @it.hex
      assert_equal(16 * 2, s.size)
      assert_match(/\A\h+\z/, s)
      33.times do |idx|
        s = @it.hex(idx)
        assert_equal(idx * 2, s.size)
        assert_match(/\A\h*\z/, s)
      end
    end

    def test_hex_encoding
      assert_equal(Encoding::US_ASCII, @it.hex.encoding)
    end

    def test_base64
      assert_equal(16, @it.base64.unpack1('m*').size)
      17.times do |idx|
        assert_equal(idx, @it.base64(idx).unpack1('m*').size)
      end
    end

    def test_urlsafe_base64
      safe = /[\n+\/]/
      65.times do |idx|
        assert_not_match(safe, @it.urlsafe_base64(idx))
      end
      # base64 can include unsafe byte
      assert((0..10000).any? {|idx| safe =~ @it.base64(idx)}, "None of base64(0..10000) is url-safe")
    end

    def test_random_number_float
      101.times do
        v = @it.random_number
        assert_in_range(0.0...1.0, v)
      end
    end

    def test_random_number_float_by_zero
      101.times do
        v = @it.random_number(0)
        assert_in_range(0.0...1.0, v)
      end
    end

    def test_random_number_int
      101.times do |idx|
        next if idx.zero?
        v = @it.random_number(idx)
        assert_in_range(0...idx, v)
      end
    end

    def test_uuid
      uuid = @it.uuid
      assert_equal(36, uuid.size)

      # Check time_hi_and_version and clock_seq_hi_res bits (RFC 4122 4.4)
      assert_equal('4', uuid[14])
      assert_include(%w'8 9 a b', uuid[19])

      assert_match(/\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/, uuid)
    end

    def assert_uuid_v7(**opts)
      t1 = current_uuid7_time(**opts)
      uuid = @it.uuid_v7(**opts)
      t3 = current_uuid7_time(**opts)

      assert_match(/\A\h{8}-\h{4}-7\h{3}-[89ab]\h{3}-\h{12}\z/, uuid)

      t2 = get_uuid7_time(uuid, **opts)
      assert_operator(t1, :<=, t2)
      assert_operator(t2, :<=, t3)
    end

    def test_uuid_v7
      assert_uuid_v7
      0.upto(12) do |extra_timestamp_bits|
        assert_uuid_v7 extra_timestamp_bits: extra_timestamp_bits
      end
    end

    # It would be nice to simply use Time#floor here.  But that is problematic
    # due to the difference between decimal vs binary fractions.
    def current_uuid7_time(extra_timestamp_bits: 0)
      denominator = (1 << extra_timestamp_bits).to_r
      Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
        .then {|ns| ((ns / 1_000_000r) * denominator).floor / denominator }
        .then {|ms| Time.at(ms / 1000r, in: "+00:00") }
    end

    def get_uuid7_time(uuid, extra_timestamp_bits: 0)
      denominator     = (1 << extra_timestamp_bits) * 1000r
      extra_chars     = extra_timestamp_bits / 4
      last_char_bits  = extra_timestamp_bits % 4
      extra_chars    += 1 if last_char_bits != 0
      timestamp_re    = /\A(\h{8})-(\h{4})-7(\h{#{extra_chars}})/
      timestamp_chars = uuid.match(timestamp_re).captures.join
      timestamp       = timestamp_chars.to_i(16)
      timestamp     >>= 4 - last_char_bits unless last_char_bits == 0
      timestamp      /= denominator
      Time.at timestamp, in: "+00:00"
    end

    def test_alphanumeric
      65.times do |n|
        an = @it.alphanumeric(n)
        assert_match(/\A[0-9a-zA-Z]*\z/, an)
        assert_equal(n, an.length)
      end
    end

    def test_alphanumeric_chars
      [
        [[*"0".."9"], /\A\d*\z/],
        [[*"a".."t"], /\A[a-t]*\z/],
        ["一二三四五六七八九十".chars, /\A[一二三四五六七八九十]*\z/],
      ].each do |chars, pattern|
        10.times do |n|
          an = @it.alphanumeric(n, chars: chars)
          assert_match(pattern, an)
          assert_equal(n, an.length)
        end
      end
    end

    def assert_in_range(range, result, mesg = nil)
      assert(range.cover?(result), build_message(mesg, "Expected #{result} to be in #{range}"))
    end
  end

  module NotDefaultTest
    def test_random_number_not_default
      msg = "random_number should not be affected by srand"
      seed = srand(0)
      x = @it.random_number(1000)
      10.times do|i|
        srand(0)
        return unless @it.random_number(1000) == x
      end
      srand(0)
      assert_not_equal(x, @it.random_number(1000), msg)
    ensure
      srand(seed) if seed
    end
  end

  class TestClassMethods < Test::Unit::TestCase
    include FormatterTest

    def setup
      @it = Random
    end
  end

  class TestInstanceMethods < Test::Unit::TestCase
    include FormatterTest
    include NotDefaultTest

    def setup
      @it = Random.new
    end
  end
end