diff options
Diffstat (limited to 'gems/gitlab-utils/spec')
-rw-r--r-- | gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb | 368 | ||||
-rw-r--r-- | gems/gitlab-utils/spec/gitlab/utils_spec.rb | 479 | ||||
-rw-r--r-- | gems/gitlab-utils/spec/gitlab/version_info_spec.rb | 193 | ||||
-rw-r--r-- | gems/gitlab-utils/spec/spec_helper.rb | 23 |
4 files changed, 1063 insertions, 0 deletions
diff --git a/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb new file mode 100644 index 00000000000..f23a12ca6a2 --- /dev/null +++ b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb @@ -0,0 +1,368 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'active_support/testing/time_helpers' + +RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do + include ActiveSupport::Testing::TimeHelpers + + let(:klass) do + strong_memoize_class = described_class + + Struct.new(:value) do + include strong_memoize_class + + def self.method_added_list + @method_added_list ||= [] + end + + def self.method_added(name) + method_added_list << name + end + + def method_name + strong_memoize(:method_name) do # rubocop:disable Gitlab/StrongMemoizeAttr + trace << value + value + end + end + + def method_name_with_expiration + strong_memoize_with_expiration(:method_name_with_expiration, 1) do + trace << value + value + end + end + + def method_name_attr + trace << value + value + end + strong_memoize_attr :method_name_attr + + def enabled? + trace << value + value + end + strong_memoize_attr :enabled? + + def method_name_with_args(*args) + strong_memoize_with(:method_name_with_args, args) do + trace << [value, args] + value + end + end + + def trace + @trace ||= [] + end + + protected + + def private_method; end + private :private_method + strong_memoize_attr :private_method + + public + + def protected_method; end + protected :protected_method + strong_memoize_attr :protected_method + + private + + def public_method; end + public :public_method + strong_memoize_attr :public_method + end + end + + subject(:object) { klass.new(value) } + + shared_examples 'caching the value' do + let(:member_name) { described_class.normalize_key(method_name) } + + it 'only calls the block once' do + value0 = object.public_send(method_name) + value1 = object.public_send(method_name) + + expect(value0).to eq(value) + expect(value1).to eq(value) + expect(object.trace).to contain_exactly(value) + end + + it 'returns and defines the instance variable for the exact value' do + returned_value = object.public_send(method_name) + memoized_value = object.instance_variable_get(:"@#{member_name}") + + expect(returned_value).to eql(value) + expect(memoized_value).to eql(value) + end + end + + describe '#strong_memoize' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + let(:method_name) { :method_name } + + it_behaves_like 'caching the value' + + it 'raises exception for invalid type as key' do + expect { object.strong_memoize(10) { 20 } }.to raise_error(/Invalid type of '10'/) + end + + it 'raises exception for invalid characters in key' do + expect { object.strong_memoize(:enabled?) { 20 } } + .to raise_error(/is not allowed as an instance variable name/) + end + end + end + + context "with memory allocation", type: :benchmark do + let(:value) { 'aaa' } + + before do + object.method_name # warmup + end + + [:method_name, "method_name"].each do |argument| + context "when argument is a #{argument.class}" do + it 'does allocate exactly one string when fetching value' do + expect do + object.strong_memoize(argument) { 10 } + end.to perform_allocation(1) + end + + it 'does allocate exactly one string when storing value' do + object.clear_memoization(:method_name) # clear to force set + + expect do + object.strong_memoize(argument) { 10 } + end.to perform_allocation(1) + end + end + end + end + end + + describe '#strong_memoize_with_expiration' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + let(:method_name) { :method_name_with_expiration } + + it_behaves_like 'caching the value' + + it 'raises exception for invalid type as key' do + expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error(/Invalid type of '10'/) + end + + it 'raises exception for invalid characters in key' do + expect { object.strong_memoize_with_expiration(:enabled?, 1) { 20 } } + .to raise_error(/is not allowed as an instance variable name/) + end + end + end + + context 'with value memoization test' do + let(:value) { 'value' } + + it 'caches the value for specified number of seconds' do + object.method_name_with_expiration + object.method_name_with_expiration + + expect(object.trace.count).to eq(1) + + travel_to(Time.current + 2.seconds) do + object.method_name_with_expiration + + expect(object.trace.count).to eq(2) + end + end + end + end + + describe '#strong_memoize_with' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + + it 'only calls the block once' do + value0 = object.method_name_with_args(1) + value1 = object.method_name_with_args(1) + value2 = object.method_name_with_args([2, 3]) + value3 = object.method_name_with_args([2, 3]) + + expect(value0).to eq(value) + expect(value1).to eq(value) + expect(value2).to eq(value) + expect(value3).to eq(value) + + expect(object.trace).to contain_exactly([value, [1]], [value, [[2, 3]]]) + end + + it 'returns and defines the instance variable for the exact value' do + returned_value = object.method_name_with_args(1, 2, 3) + memoized_value = object.instance_variable_get(:@method_name_with_args) + + expect(returned_value).to eql(value) + expect(memoized_value).to eql({ [[1, 2, 3]] => value }) + end + end + end + end + + describe '#strong_memoized?' do + shared_examples 'memoization check' do |method_name| + context "when method is :#{method_name}" do + let(:value) { :anything } + + subject { object.strong_memoized?(method_name) } + + it 'returns false if the value is uncached' do + expect(subject).to be(false) + end + + it 'returns true if the value is cached' do + object.public_send(method_name) + + expect(subject).to be(true) + end + end + end + + it_behaves_like 'memoization check', :method_name + it_behaves_like 'memoization check', :enabled? + end + + describe '#clear_memoization' do + shared_examples 'clearing memoization' do |method_name| + let(:member_name) { described_class.normalize_key(method_name) } + let(:value) { 'mepmep' } + + it 'removes the instance variable' do + object.public_send(method_name) + + object.clear_memoization(method_name) + + expect(object.instance_variable_defined?(:"@#{member_name}")).to be(false) + end + end + + it_behaves_like 'clearing memoization', :method_name + it_behaves_like 'clearing memoization', :enabled? + end + + describe '.strong_memoize_attr' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value '#{value}'" do + let(:value) { value } + + context 'with memoized after method definition' do + let(:method_name) { :method_name_attr } + + it_behaves_like 'caching the value' + + it 'calls the existing .method_added' do + expect(klass.method_added_list).to include(:method_name_attr) + end + + it 'retains method arity' do + expect(klass.instance_method(method_name).arity).to eq(0) + end + end + end + end + + describe 'method visibility' do + it 'sets private visibility' do + expect(klass.private_instance_methods).to include(:private_method) + expect(klass.protected_instance_methods).not_to include(:private_method) + expect(klass.public_instance_methods).not_to include(:private_method) + end + + it 'sets protected visibility' do + expect(klass.private_instance_methods).not_to include(:protected_method) + expect(klass.protected_instance_methods).to include(:protected_method) + expect(klass.public_instance_methods).not_to include(:protected_method) + end + + it 'sets public visibility' do + expect(klass.private_instance_methods).not_to include(:public_method) + expect(klass.protected_instance_methods).not_to include(:public_method) + expect(klass.public_instance_methods).to include(:public_method) + end + end + + context "when method doesn't exist" do + let(:klass) do + strong_memoize_class = described_class + + Struct.new(:value) do + include strong_memoize_class + end + end + + subject { klass.strong_memoize_attr(:nonexistent_method) } + + it 'fails when strong-memoizing a nonexistent method' do + expect { subject }.to raise_error(NameError, /undefined method `nonexistent_method' for class/) + end + end + + context 'when memoized method has parameters' do + it 'raises an error' do + expected_message = /Using `strong_memoize_attr` on methods with parameters is not supported/ + + expect do + strong_memoize_class = described_class + + Class.new do + include strong_memoize_class + + def method_with_parameters(params); end + strong_memoize_attr :method_with_parameters + end + end.to raise_error(RuntimeError, expected_message) + end + end + end + + describe '.normalize_key' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.normalize_key(input) } + + where(:input, :output, :valid) do + :key | :key | true + "key" | "key" | true + :key? | "key?" | true + "key?" | "key?" | true + :key! | "key!" | true + "key!" | "key!" | true + # invalid cases caught elsewhere + :"ke?y" | :"ke?y" | false + "ke?y" | "ke?y" | false + :"ke!y" | :"ke!y" | false + "ke!y" | "ke!y" | false + end + + with_them do + let(:ivar) { "@#{output}" } + + it { is_expected.to eq(output) } + + if params[:valid] + it 'is a valid ivar name' do + expect { instance_variable_defined?(ivar) }.not_to raise_error + end + else + it 'raises a NameError error' do + expect { instance_variable_defined?(ivar) } + .to raise_error(NameError, /not allowed as an instance/) + end + end + end + end +end diff --git a/gems/gitlab-utils/spec/gitlab/utils_spec.rb b/gems/gitlab-utils/spec/gitlab/utils_spec.rb new file mode 100644 index 00000000000..53593190eea --- /dev/null +++ b/gems/gitlab-utils/spec/gitlab/utils_spec.rb @@ -0,0 +1,479 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Utils, feature_category: :shared do + using RSpec::Parameterized::TableSyntax + include StubENV + + delegate :to_boolean, :boolean_to_yes_no, :slugify, :which, + :ensure_array_from_string, :bytes_to_megabytes, + :append_path, :remove_leading_slashes, :allowlisted?, + :decode_path, :ms_to_round_sec, to: :described_class + + describe '.allowlisted?' do + let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd'] } + + it 'returns true if path is allowed' do + expect(allowlisted?('/foo/bar', allowed_paths)).to be(true) + end + + it 'returns false if path is not allowed' do + expect(allowlisted?('/test/test', allowed_paths)).to be(false) + end + end + + describe '.decode_path' do + it 'returns path unencoded for singled-encoded paths' do + expect(decode_path('%2Fhome%2Fbar%3Fasd%3Dqwe')).to eq('/home/bar?asd=qwe') + end + + it 'returns path when it is unencoded' do + expect(decode_path('/home/bar?asd=qwe')).to eq('/home/bar?asd=qwe') + end + + [ + '..%252F..%252F..%252Fetc%252Fpasswd', + '%25252Fresult%25252Fchosennickname%25253D%252522jj%252522' + ].each do |multiple_encoded_path| + it 'raises an exception when the path is multiple-encoded' do + expect { decode_path(multiple_encoded_path) }.to raise_error(/path #{multiple_encoded_path} is not allowed/) + end + end + end + + describe '.slugify' do + { + 'TEST' => 'test', + 'project_with_underscores' => 'project-with-underscores', + 'namespace/project' => 'namespace-project', + 'a' * 70 => 'a' * 63, + 'test_trailing_' => 'test-trailing' + }.each do |original, expected| + it "slugifies #{original} to #{expected}" do + expect(slugify(original)).to eq(expected) + end + end + end + + describe '.ms_to_round_sec' do + where(:original, :expected) do + 1999.8999 | 1.9999 + 12384 | 12.384 + 333 | 0.333 + 1333.33333333 | 1.333333 + end + + with_them do + it "returns rounded seconds" do + expect(ms_to_round_sec(original)).to eq(expected) + end + end + end + + describe '.nlbr' do + it 'replaces new lines with <br>' do + expect(described_class.nlbr("<b>hello</b>\n<i>world</i>")).to eq("hello<br>world") + end + end + + describe '.remove_line_breaks' do + where(:original, :expected) do + "foo\nbar\nbaz" | "foobarbaz" + "foo\r\nbar\r\nbaz" | "foobarbaz" + "foobar" | "foobar" + end + + with_them do + it "replace line breaks with an empty string" do + expect(described_class.remove_line_breaks(original)).to eq(expected) + end + end + end + + describe '.to_boolean' do + it 'accepts booleans' do + expect(to_boolean(true)).to be(true) + expect(to_boolean(false)).to be(false) + end + + it 'converts a valid value to a boolean' do + expect(to_boolean(true)).to be(true) + expect(to_boolean('true')).to be(true) + expect(to_boolean('YeS')).to be(true) + expect(to_boolean('t')).to be(true) + expect(to_boolean('1')).to be(true) + expect(to_boolean(1)).to be(true) + expect(to_boolean('ON')).to be(true) + + expect(to_boolean('FaLse')).to be(false) + expect(to_boolean('F')).to be(false) + expect(to_boolean('NO')).to be(false) + expect(to_boolean('n')).to be(false) + expect(to_boolean('0')).to be(false) + expect(to_boolean(0)).to be(false) + expect(to_boolean('oFF')).to be(false) + end + + it 'converts an invalid value to nil' do + expect(to_boolean('fals')).to be_nil + expect(to_boolean('yeah')).to be_nil + expect(to_boolean('')).to be_nil + expect(to_boolean(nil)).to be_nil + end + + it 'accepts a default value, and does not return it when a valid value is given' do + expect(to_boolean(true, default: false)).to be(true) + expect(to_boolean('true', default: false)).to be(true) + expect(to_boolean('YeS', default: false)).to be(true) + expect(to_boolean('t', default: false)).to be(true) + expect(to_boolean('1', default: 'any value')).to be(true) + expect(to_boolean('ON', default: 42)).to be(true) + + expect(to_boolean('FaLse', default: true)).to be(false) + expect(to_boolean('F', default: true)).to be(false) + expect(to_boolean('NO', default: true)).to be(false) + expect(to_boolean('n', default: true)).to be(false) + expect(to_boolean('0', default: 'any value')).to be(false) + expect(to_boolean('oFF', default: 42)).to be(false) + end + + it 'accepts a default value, and returns it when an invalid value is given' do + expect(to_boolean('fals', default: true)).to eq(true) + expect(to_boolean('yeah', default: false)).to eq(false) + expect(to_boolean('', default: 'any value')).to eq('any value') + expect(to_boolean(nil, default: 42)).to eq(42) + end + end + + describe '.boolean_to_yes_no' do + it 'converts booleans to Yes or No' do + expect(boolean_to_yes_no(true)).to eq('Yes') + expect(boolean_to_yes_no(false)).to eq('No') + end + end + + describe '.which' do + before do + stub_env('PATH', '/sbin:/usr/bin:/home/joe/bin') + end + + it 'finds the full path to an executable binary in order of appearance' do + expect(File).to receive(:executable?).with('/sbin/tool').ordered.and_return(false) + expect(File).to receive(:executable?).with('/usr/bin/tool').ordered.and_return(true) + expect(File).not_to receive(:executable?).with('/home/joe/bin/tool') + + expect(which('tool')).to eq('/usr/bin/tool') + end + end + + describe '.ensure_array_from_string' do + it 'returns the same array if given one' do + arr = ['a', 4, true, { test: 1 }] + + expect(ensure_array_from_string(arr)).to eq(arr) + end + + it 'turns comma-separated strings into arrays' do + str = 'seven, eight, 9, 10' + + expect(ensure_array_from_string(str)).to eq(%w[seven eight 9 10]) + end + end + + describe '.bytes_to_megabytes' do + it 'converts bytes to megabytes' do + bytes = 1.megabyte + + expect(bytes_to_megabytes(bytes)).to eq(1) + end + end + + describe '.append_path' do + where(:host, :path, :result) do + 'http://test/' | '/foo/bar' | 'http://test/foo/bar' + 'http://test/' | '//foo/bar' | 'http://test/foo/bar' + 'http://test//' | '/foo/bar' | 'http://test/foo/bar' + 'http://test' | 'foo/bar' | 'http://test/foo/bar' + 'http://test//' | '' | 'http://test/' + 'http://test//' | nil | 'http://test/' + '' | '/foo/bar' | '/foo/bar' + nil | '/foo/bar' | '/foo/bar' + end + + with_them do + it 'makes sure there is only one slash as path separator' do + expect(append_path(host, path)).to eq(result) + end + end + end + + describe '.remove_leading_slashes' do + where(:str, :result) do + '/foo/bar' | 'foo/bar' + '//foo/bar' | 'foo/bar' + '/foo/bar/' | 'foo/bar/' + 'foo/bar' | 'foo/bar' + '' | '' + nil | '' + end + + with_them do + it 'removes leading slashes' do + expect(remove_leading_slashes(str)).to eq(result) + end + end + end + + describe '.ensure_utf8_size' do + context 'with string is has less bytes than expected' do + it 'backfills string with null characters' do + transformed = described_class.ensure_utf8_size('a' * 10, bytes: 32) + + expect(transformed.bytesize).to eq 32 + expect(transformed).to eq(('a' * 10) + ('0' * 22)) + end + end + + context 'with string size is exactly the one that is expected' do + it 'returns original value' do + transformed = described_class.ensure_utf8_size('a' * 32, bytes: 32) + + expect(transformed).to eq 'a' * 32 + expect(transformed.bytesize).to eq 32 + end + end + + context 'when string contains a few multi-byte UTF characters' do + it 'backfills string with null characters' do + transformed = described_class.ensure_utf8_size('❤' * 6, bytes: 32) + + expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14) # rubocop:disable Style/StringConcatenation + expect(transformed.bytesize).to eq 32 + end + end + + context 'when string has multiple multi-byte UTF chars exceeding 32 bytes' do + it 'truncates string to 32 characters and backfills it if needed' do + transformed = described_class.ensure_utf8_size('❤' * 18, bytes: 32) + + expect(transformed).to eq(('❤' * 10) + ('0' * 2)) + expect(transformed.bytesize).to eq 32 + end + end + end + + describe '.deep_indifferent_access' do + let(:hash) do + { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] } + end + + subject { described_class.deep_indifferent_access(hash) } + + it 'allows to access hash keys with symbols' do + expect(subject[:variables]).to be_a(Array) + end + + it 'allows to access array keys with symbols' do + expect(subject[:variables].first[:key]).to eq('VAR1') + end + end + + describe '.deep_symbolized_access' do + let(:hash) do + { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] } + end + + subject { described_class.deep_symbolized_access(hash) } + + it 'allows to access hash keys with symbols' do + expect(subject[:variables]).to be_a(Array) + end + + it 'allows to access array keys with symbols' do + expect(subject[:variables].first[:key]).to eq('VAR1') + end + end + + describe '.try_megabytes_to_bytes' do + context 'when the size can be converted to megabytes' do + it 'returns the size in megabytes' do + size = described_class.try_megabytes_to_bytes(1) + + expect(size).to eq(1.megabytes) + end + end + + context 'when the size can not be converted to megabytes' do + it 'returns the input size' do + size = described_class.try_megabytes_to_bytes('foo') + + expect(size).to eq('foo') + end + end + end + + describe '.string_to_ip_object' do + it 'returns nil when string is nil' do + expect(described_class.string_to_ip_object(nil)).to eq(nil) + end + + it 'returns nil when string is invalid IP' do + expect(described_class.string_to_ip_object('invalid ip')).to eq(nil) + expect(described_class.string_to_ip_object('')).to eq(nil) + end + + it 'returns IP object when string is valid IP' do + expect(described_class.string_to_ip_object('192.168.1.1')).to eq(IPAddr.new('192.168.1.1')) + expect(described_class.string_to_ip_object('::ffff:a9fe:a864')).to eq(IPAddr.new('::ffff:a9fe:a864')) + expect(described_class.string_to_ip_object('[::ffff:a9fe:a864]')).to eq(IPAddr.new('::ffff:a9fe:a864')) + expect(described_class.string_to_ip_object('127.0.0.0/28')).to eq(IPAddr.new('127.0.0.0/28')) + expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124')) + end + end + + describe ".safe_downcase!" do + where(:str, :result) do + "test" | "test" + "Test" | "test" + "test" | "test" + "Test" | "test" + end + + with_them do + it "downcases the string" do + expect(described_class.safe_downcase!(str)).to eq(result) + end + end + end + + describe '.parse_url' do + it 'returns Addressable::URI object' do + expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI) + end + + it 'returns nil when URI cannot be parsed' do + expect(described_class.parse_url('://gitlab.com')).to be nil + end + + it 'returns nil with invalid parameter' do + expect(described_class.parse_url(1)).to be nil + end + end + + describe '.add_url_parameters' do + subject { described_class.add_url_parameters(url, params) } + + where(:url, :params, :expected_url) do + nil | nil | '' + nil | { b: 3, a: 2 } | '?a=2&b=3' + 'https://gitlab.com' | nil | 'https://gitlab.com' + 'https://gitlab.com' | { b: 3, a: 2 } | 'https://gitlab.com?a=2&b=3' + 'https://gitlab.com?a=1#foo' | { b: 3, 'a' => 2 } | 'https://gitlab.com?a=2&b=3#foo' + 'https://gitlab.com?a=1#foo' | [[:b, 3], [:a, 2]] | 'https://gitlab.com?a=2&b=3#foo' + end + + with_them do + it { is_expected.to eq(expected_url) } + end + end + + describe '.removes_sensitive_data_from_url' do + it 'returns string object' do + expect(described_class.removes_sensitive_data_from_url('http://gitlab.com')).to be_instance_of(String) + end + + it 'returns nil when URI cannot be parsed' do + expect(described_class.removes_sensitive_data_from_url('://gitlab.com')).to be nil + end + + it 'returns nil with invalid parameter' do + expect(described_class.removes_sensitive_data_from_url(1)).to be nil + end + + it 'returns string with filtered access_token param' do + expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token')) + .to eq('http://gitlab.com/auth.html#access_token=filtered') + end + + it 'returns string with filtered access_token param but other params preserved' do + expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token&token_type=Bearer&state=test')) + .to include('&token_type=Bearer', '&state=test') + end + end + + describe 'multiple_key_invert' do + it 'invert keys with array values' do + hash = { + dast: [:vulnerabilities_count, :scanned_resources_count], + sast: [:vulnerabilities_count] + } + expect(described_class.multiple_key_invert(hash)).to eq({ + vulnerabilities_count: [:dast, :sast], + scanned_resources_count: [:dast] + }) + end + end + + describe '.stable_sort_by' do + subject(:sorted_list) { described_class.stable_sort_by(list) { |obj| obj[:priority] } } + + context 'when items have the same priority' do + let(:list) do + [ + { name: 'obj 1', priority: 1 }, + { name: 'obj 2', priority: 1 }, + { name: 'obj 3', priority: 1 } + ] + end + + it 'does not change order in cases of ties' do + expect(sorted_list).to eq(list) + end + end + + context 'when items have different priorities' do + let(:list) do + [ + { name: 'obj 1', priority: 2 }, + { name: 'obj 2', priority: 1 }, + { name: 'obj 3', priority: 3 } + ] + end + + it 'sorts items like the regular sort_by' do + expect(sorted_list).to eq( + [ + { name: 'obj 2', priority: 1 }, + { name: 'obj 1', priority: 2 }, + { name: 'obj 3', priority: 3 } + ]) + end + end + end + + describe '.valid_brackets?' do + where(:input, :allow_nested, :valid) do + 'no brackets' | true | true + 'no brackets' | false | true + 'user[avatar]' | true | true + 'user[avatar]' | false | true + 'user[avatar][friends]' | true | true + 'user[avatar][friends]' | false | true + 'user[avatar[image[url]]]' | true | true + 'user[avatar[image[url]]]' | false | false + 'user[avatar[]friends]' | true | true + 'user[avatar[]friends]' | false | false + 'user[avatar]]' | true | false + 'user[avatar]]' | false | false + 'user][avatar]]' | true | false + 'user][avatar]]' | false | false + 'user[avatar' | true | false + 'user[avatar' | false | false + end + + with_them do + it { expect(described_class.valid_brackets?(input, allow_nested: allow_nested)).to eq(valid) } + end + end +end diff --git a/gems/gitlab-utils/spec/gitlab/version_info_spec.rb b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb new file mode 100644 index 00000000000..2b5f6bcb4c1 --- /dev/null +++ b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::VersionInfo, feature_category: :shared do + before do + @unknown = described_class.new + @v0_0_1 = described_class.new(0, 0, 1) + @v0_1_0 = described_class.new(0, 1, 0) + @v1_0_0 = described_class.new(1, 0, 0) + @v1_0_1 = described_class.new(1, 0, 1) + @v1_0_1_b1 = described_class.new(1, 0, 1, '-b1') + @v1_0_1_rc1 = described_class.new(1, 0, 1, '-rc1') + @v1_0_1_rc2 = described_class.new(1, 0, 1, '-rc2') + @v1_1_0 = described_class.new(1, 1, 0) + @v1_1_0_beta1 = described_class.new(1, 1, 0, '-beta1') + @v2_0_0 = described_class.new(2, 0, 0) + @v13_10_1_1574_89 = described_class.parse("v13.10.1~beta.1574.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_89 = described_class.parse("v13.10.1~beta.1575.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_90 = described_class.parse("v13.10.1~beta.1575.gf6ea9390", parse_suffix: true) + end + + describe '>' do + it { expect(@v2_0_0).to be > @v1_1_0 } + it { expect(@v1_1_0).to be > @v1_0_1 } + it { expect(@v1_0_1_b1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_1_b1 } + it { expect(@v1_0_1_rc2).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc2 } + it { expect(@v1_0_1).to be > @v1_0_0 } + it { expect(@v1_0_0).to be > @v0_1_0 } + it { expect(@v1_1_0_beta1).to be > @v1_0_1_rc2 } + it { expect(@v1_1_0).to be > @v1_1_0_beta1 } + it { expect(@v0_1_0).to be > @v0_0_1 } + end + + describe '>=' do + it { expect(@v2_0_0).to be >= described_class.new(2, 0, 0) } + it { expect(@v2_0_0).to be >= @v1_1_0 } + it { expect(@v1_0_1_rc2).to be >= @v1_0_1_rc1 } + end + + describe '<' do + it { expect(@v0_0_1).to be < @v0_1_0 } + it { expect(@v0_1_0).to be < @v1_0_0 } + it { expect(@v1_0_0).to be < @v1_0_1 } + it { expect(@v1_0_1).to be < @v1_1_0 } + it { expect(@v1_0_0).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc2).to be < @v1_0_1 } + it { expect(@v1_1_0).to be < @v2_0_0 } + it { expect(@v13_10_1_1574_89).to be < @v13_10_1_1575_89 } + it { expect(@v13_10_1_1575_89).to be < @v13_10_1_1575_90 } + end + + describe '<=' do + it { expect(@v0_0_1).to be <= described_class.new(0, 0, 1) } + it { expect(@v0_0_1).to be <= @v0_1_0 } + it { expect(@v1_0_1_b1).to be <= @v1_0_1_rc1 } + it { expect(@v1_0_1_rc1).to be <= @v1_0_1_rc2 } + it { expect(@v1_1_0_beta1).to be <= @v1_1_0 } + end + + describe '==' do + it { expect(@v0_0_1).to eq(described_class.new(0, 0, 1)) } + it { expect(@v0_1_0).to eq(described_class.new(0, 1, 0)) } + it { expect(@v1_0_0).to eq(described_class.new(1, 0, 0)) } + it { expect(@v1_0_1_rc1).to eq(described_class.new(1, 0, 1, '-rc1')) } + end + + describe '!=' do + it { expect(@v0_0_1).not_to eq(@v0_1_0) } + it { expect(@v1_0_1_rc1).not_to eq(@v1_0_1_rc2) } + end + + describe '.unknown' do + it { expect(@unknown).not_to be @v0_0_1 } + it { expect(@unknown).not_to be described_class.new } + it { expect { @unknown > @v0_0_1 }.to raise_error(ArgumentError) } + it { expect { @unknown < @v0_0_1 }.to raise_error(ArgumentError) } + end + + describe '.parse' do + it { expect(described_class.parse(described_class.new(1, 0, 0))).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0.0b1")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0b1")).not_to be_valid } + it { expect(described_class.parse("1.1.#{'1' * described_class::MAX_VERSION_LENGTH}")).not_to be_valid } + it { expect(described_class.parse(nil)).not_to be_valid } + + context 'with parse_suffix: true' do + let(:versions) do + <<-VERSIONS.lines + 0.0.1 + 0.1.0 + 1.0.0 + 1.0.1-b1 + 1.0.1-rc1 + 1.0.1-rc2 + 1.0.1 + 1.1.0-beta1 + 1.1.0 + 2.0.0 + v13.10.0-pre + v13.10.0-rc1 + v13.10.0-rc2 + v13.10.0 + v13.10.1~beta.1574.gf6ea9389 + v13.10.1~beta.1575.gf6ea9389 + v13.10.1-rc1 + v13.10.1-rc2 + v13.10.1 + VERSIONS + end + + let(:parsed_versions) do + versions.map(&:strip).map { |version| described_class.parse(version, parse_suffix: true) } + end + + it 'versions are returned in a correct order' do + expect(parsed_versions.shuffle.sort).to eq(parsed_versions) + end + end + end + + describe '.to_s' do + it { expect(@v1_0_0.to_s).to eq("1.0.0") } + it { expect(@v1_0_1_rc1.to_s).to eq("1.0.1-rc1") } + it { expect(@unknown.to_s).to eq("Unknown") } + end + + describe '.to_json' do + let(:correct_version) do + "{\"major\":1,\"minor\":0,\"patch\":1}" + end + + let(:unknown_version) do + "{\"major\":0,\"minor\":0,\"patch\":0}" + end + + it { expect(@v1_0_1.to_json).to eq(correct_version) } + it { expect(@v1_0_1_rc2.to_json).to eq(correct_version) } + it { expect(@unknown.to_json).to eq(unknown_version) } + end + + describe '.hash' do + it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) } + it { expect(described_class.parse("1.0.1-rc1", parse_suffix: true).hash).to eq(@v1_0_1_rc1.hash) } + end + + describe '.eql?' do + it { expect(described_class.parse("1.0.0").eql?(@v1_0_0)).to be_truthy } + it { expect(described_class.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc1)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc2)).to be_falsey } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1)).to be_falsey } + it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy } + it { expect([@v1_0_0, @v1_1_0, @v1_0_0, @v1_0_1_rc1, @v1_0_1_rc1].uniq).to eq [@v1_0_0, @v1_1_0, @v1_0_1_rc1] } + end + + describe '.same_minor_version?' do + it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey } + it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy } + it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey } + it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey } + end + + describe '.without_patch' do + it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) } + it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1_rc1.without_patch).to eq(@v1_0_0) } + end + + describe 'MAX_VERSION_LENGTH' do + subject { described_class::MAX_VERSION_LENGTH } + + it { is_expected.to eq(128) } + end +end diff --git a/gems/gitlab-utils/spec/spec_helper.rb b/gems/gitlab-utils/spec/spec_helper.rb new file mode 100644 index 00000000000..5dc3859f77d --- /dev/null +++ b/gems/gitlab-utils/spec/spec_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails' +require 'rspec/mocks' +require 'rspec-benchmark' +require 'rspec-parameterized' + +require 'gitlab/rspec/all' +require 'gitlab/utils/all' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers + + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end |