diff --git a/Gemfile b/Gemfile index e7dffa5f72f..d75051b3db8 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,6 @@ gem 'newrelic_rpm' gem 'nokogiri', '>=1.10.5' gem 'oj' gem 'openssl', '>= 3.2' -gem 'palm_civet' gem 'prometheus-client' gem 'public_suffix' gem 'puma' diff --git a/Gemfile.lock b/Gemfile.lock index f9234801379..1a2b912bcc3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -349,7 +349,6 @@ GEM openssl (4.0.0) os (1.1.4) ostruct (0.6.3) - palm_civet (1.1.0) parallel (1.27.0) parallel_tests (5.5.0) parallel @@ -660,7 +659,6 @@ DEPENDENCIES nokogiri (>= 1.10.5) oj openssl (>= 3.2) - palm_civet parallel_tests pg prometheus-client @@ -705,4 +703,4 @@ DEPENDENCIES webrick (~> 1.9.2) BUNDLED WITH - 2.4.19 + 2.6.9 diff --git a/lib/cloud_controller/app_manifest/byte_converter.rb b/lib/cloud_controller/app_manifest/byte_converter.rb index fc6f8b1be46..32c0661bd74 100644 --- a/lib/cloud_controller/app_manifest/byte_converter.rb +++ b/lib/cloud_controller/app_manifest/byte_converter.rb @@ -1,4 +1,4 @@ -require 'palm_civet' +require 'cloud_controller/byte_quantity' module VCAP::CloudController class ByteConverter @@ -10,8 +10,8 @@ def convert_to_mb(human_readable_byte_value) return nil if human_readable_byte_value.blank? raise NonNumericError unless human_readable_byte_value.to_s.match?(/\A-?\d+(?:\.\d+)?/) - PalmCivet.to_megabytes(human_readable_byte_value.to_s) - rescue PalmCivet::InvalidByteQuantityError + ByteQuantity.to_megabytes(human_readable_byte_value.to_s) + rescue ByteQuantity::InvalidByteQuantityError raise InvalidUnitsError end @@ -19,8 +19,8 @@ def convert_to_b(human_readable_byte_value) return nil if human_readable_byte_value.blank? raise NonNumericError unless human_readable_byte_value.to_s.match?(/\A-?\d+(?:\.\d+)?/) - PalmCivet.to_bytes(human_readable_byte_value.to_s) - rescue PalmCivet::InvalidByteQuantityError + ByteQuantity.to_bytes(human_readable_byte_value.to_s) + rescue ByteQuantity::InvalidByteQuantityError raise InvalidUnitsError end diff --git a/lib/cloud_controller/byte_quantity.rb b/lib/cloud_controller/byte_quantity.rb new file mode 100644 index 00000000000..6d752f2e751 --- /dev/null +++ b/lib/cloud_controller/byte_quantity.rb @@ -0,0 +1,92 @@ +# Derived from the palm_civet library +# Copyright (c) 2013 Anand Gaitonde +# Licensed under the MIT License +# https://github.com/goodmustache/palm_civet + +module VCAP + module CloudController + module ByteQuantity + BYTE = 1.0 + KILOBYTE = 1024 * BYTE + MEGABYTE = 1024 * KILOBYTE + GIGABYTE = 1024 * MEGABYTE + TERABYTE = 1024 * GIGABYTE + BYTESPATTERN = /^(-?\d+(?:\.\d+)?)([KMGT]i?B?|B)$/i + + class InvalidByteQuantityError < RuntimeError + def initialize(msg='byte quantity must be a positive integer with a unit of measurement like M, MB, MiB, G, GiB, or GB') + super + end + end + + # Returns a human-readable byte string of the form 10M, 12.5K, and so forth. + # The following units are available: + # * T: Terabyte + # * G: Gigabyte + # * M: Megabyte + # * K: Kilobyte + # * B: Byte + # The unit that results in the smallest number greater than or equal to 1 is + # always chosen. + def self.byte_size(bytes) + raise TypeError.new('must be an integer or float') unless bytes.is_a? Numeric + + case + when bytes >= TERABYTE + unit = 'T' + value = bytes / TERABYTE + when bytes >= GIGABYTE + unit = 'G' + value = bytes / GIGABYTE + when bytes >= MEGABYTE + unit = 'M' + value = bytes / MEGABYTE + when bytes >= KILOBYTE + unit = 'K' + value = bytes / KILOBYTE + when bytes >= BYTE + unit = 'B' + value = bytes + else + return '0' + end + + value = sprintf('%g', sprintf('%.1f', value)) + value << unit + end + + # Parses a string formatted by bytes_size as bytes. Note binary-prefixed and + # SI prefixed units both mean a base-2 units: + # * KB = K = KiB = 1024 + # * MB = M = MiB = 1024 * K + # * GB = G = GiB = 1024 * M + # * TB = T = TiB = 1024 * G + def self.to_bytes(bytes) + matches = BYTESPATTERN.match(bytes.strip) + raise InvalidByteQuantityError if matches.nil? + + value = Float(matches[1]) + + case matches[2][0].capitalize + when 'T' + value *= TERABYTE + when 'G' + value *= GIGABYTE + when 'M' + value *= MEGABYTE + when 'K' + value *= KILOBYTE + end + + value.to_i + rescue TypeError + raise InvalidByteQuantityError + end + + # Parses a string formatted by byte_size as megabytes. + def self.to_megabytes(bytes) + (to_bytes(bytes) / MEGABYTE).to_i + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/byte_quantity_spec.rb b/spec/unit/lib/cloud_controller/byte_quantity_spec.rb new file mode 100644 index 00000000000..bf99ae01d33 --- /dev/null +++ b/spec/unit/lib/cloud_controller/byte_quantity_spec.rb @@ -0,0 +1,166 @@ +# Derived from the palm_civet library +# Copyright (c) 2013 Anand Gaitonde +# Licensed under the MIT License +# https://github.com/goodmustache/palm_civet + +require 'spec_helper' +require 'cloud_controller/byte_quantity' + +module VCAP::CloudController + RSpec.describe ByteQuantity do + describe '#byte_size' do + it 'prints in the largest possible unit' do + expect(ByteQuantity.byte_size(10 * ByteQuantity::TERABYTE)).to eq('10T') + expect(ByteQuantity.byte_size(10.5 * ByteQuantity::TERABYTE)).to eq('10.5T') + + expect(ByteQuantity.byte_size(10 * ByteQuantity::GIGABYTE)).to eq('10G') + expect(ByteQuantity.byte_size(10.5 * ByteQuantity::GIGABYTE)).to eq('10.5G') + + expect(ByteQuantity.byte_size(100 * ByteQuantity::MEGABYTE)).to eq('100M') + expect(ByteQuantity.byte_size(100.5 * ByteQuantity::MEGABYTE)).to eq('100.5M') + + expect(ByteQuantity.byte_size(100 * ByteQuantity::KILOBYTE)).to eq('100K') + expect(ByteQuantity.byte_size(100.5 * ByteQuantity::KILOBYTE)).to eq('100.5K') + + expect(ByteQuantity.byte_size(1)).to eq('1B') + end + + it "prints '0' for zero bytes" do + expect(ByteQuantity.byte_size(0)).to eq('0') + end + + it 'raises a type error on non-number values' do + expect do + ByteQuantity.byte_size('something else') + end.to raise_error(TypeError, 'must be an integer or float') + end + end + + describe '#to_bytes' do + it 'parses byte amounts with short units (e.g. M, G)' do + expect(ByteQuantity.to_bytes('5B')).to eq(5) + expect(ByteQuantity.to_bytes('5K')).to eq(5 * ByteQuantity::KILOBYTE) + expect(ByteQuantity.to_bytes('5M')).to eq(5 * ByteQuantity::MEGABYTE) + expect(ByteQuantity.to_bytes('5G')).to eq(5 * ByteQuantity::GIGABYTE) + expect(ByteQuantity.to_bytes('5T')).to eq(5 * ByteQuantity::TERABYTE) + end + + it 'parses byte amounts that are float (e.g. 5.3KB)' do + expect(ByteQuantity.to_bytes('13.5KB')).to eq(13_824) + expect(ByteQuantity.to_bytes('4.5KB')).to eq(4608) + expect(ByteQuantity.to_bytes('2.55KB')).to eq(2611) + end + + it 'parses byte amounts with long units (e.g MB, GB)' do + expect(ByteQuantity.to_bytes('5MB')).to eq(5 * ByteQuantity::MEGABYTE) + expect(ByteQuantity.to_bytes('5mb')).to eq(5 * ByteQuantity::MEGABYTE) + expect(ByteQuantity.to_bytes('2GB')).to eq(2 * ByteQuantity::GIGABYTE) + expect(ByteQuantity.to_bytes('3TB')).to eq(3 * ByteQuantity::TERABYTE) + end + + it 'parses byte amounts with long binary units (e.g MiB, GiB)' do + expect(ByteQuantity.to_bytes('5MiB')).to eq(5 * ByteQuantity::MEGABYTE) + expect(ByteQuantity.to_bytes('5mib')).to eq(5 * ByteQuantity::MEGABYTE) + expect(ByteQuantity.to_bytes('2GiB')).to eq(2 * ByteQuantity::GIGABYTE) + expect(ByteQuantity.to_bytes('3TiB')).to eq(3 * ByteQuantity::TERABYTE) + end + + it 'allows whitespace before and after the value' do + expect(ByteQuantity.to_bytes("\t\n\r 5MB ")).to eq(5 * ByteQuantity::MEGABYTE) + end + + context 'when the byte amount is 0' do + it 'returns 0 bytes' do + expect(ByteQuantity.to_bytes('0TB')).to eq(0) + end + end + + context 'when the byte amount is negative' do + it 'returns a negative amount of bytes' do + expect(ByteQuantity.to_bytes('-200B')).to eq(-200) + end + end + + context 'when it raises an error' do + it 'raises when the unit is missing' do + expect do + ByteQuantity.to_bytes('5') + end.to raise_error(ByteQuantity::InvalidByteQuantityError) + end + + it 'raises when the unit is unrecognized' do + expect do + ByteQuantity.to_bytes('5MBB') + end.to raise_error(ByteQuantity::InvalidByteQuantityError) + + expect do + ByteQuantity.to_bytes('5BB') + end.to raise_error(ByteQuantity::InvalidByteQuantityError) + end + end + end + + describe '#to_megabytes' do + it 'parses byte amounts with short units (e.g. M, G)' do + expect(ByteQuantity.to_megabytes('5B')).to eq(0) + expect(ByteQuantity.to_megabytes('5K')).to eq(0) + expect(ByteQuantity.to_megabytes('5M')).to eq(5) + expect(ByteQuantity.to_megabytes('5m')).to eq(5) + expect(ByteQuantity.to_megabytes('5G')).to eq(5120) + expect(ByteQuantity.to_megabytes('5T')).to eq(5_242_880) + end + + it 'parses byte amounts with long units (e.g MB, GB)' do + expect(ByteQuantity.to_megabytes('5B')).to eq(0) + expect(ByteQuantity.to_megabytes('5KB')).to eq(0) + expect(ByteQuantity.to_megabytes('5MB')).to eq(5) + expect(ByteQuantity.to_megabytes('5mb')).to eq(5) + expect(ByteQuantity.to_megabytes('5GB')).to eq(5120) + expect(ByteQuantity.to_megabytes('5TB')).to eq(5_242_880) + end + + it 'parses byte amounts with long binary units (e.g MiB, GiB)' do + expect(ByteQuantity.to_megabytes('5B')).to eq(0) + expect(ByteQuantity.to_megabytes('5KiB')).to eq(0) + expect(ByteQuantity.to_megabytes('5MiB')).to eq(5) + expect(ByteQuantity.to_megabytes('5mib')).to eq(5) + expect(ByteQuantity.to_megabytes('5GiB')).to eq(5120) + expect(ByteQuantity.to_megabytes('5TiB')).to eq(5_242_880) + end + + it 'allows whitespace before and after the value' do + expect(ByteQuantity.to_megabytes("\t\n\r 5MB ")).to eq(5) + end + + context 'when the byte amount is 0' do + it 'returns 0 megabytes' do + expect(ByteQuantity.to_megabytes('0TB')).to eq(0) + end + end + + context 'when the byte amount is negative' do + it 'returns a negative amount of megabytes' do + expect(ByteQuantity.to_megabytes('-200MB')).to eq(-200) + end + end + + context 'when it raises an error' do + it 'raises when the unit is missing' do + expect do + ByteQuantity.to_megabytes('5') + end.to raise_error(ByteQuantity::InvalidByteQuantityError) + end + + it 'raises when the unit is unrecognized' do + expect do + ByteQuantity.to_megabytes('5MBB') + end.to raise_error(ByteQuantity::InvalidByteQuantityError) + + expect do + ByteQuantity.to_megabytes('5BB') + end.to raise_error(ByteQuantity::InvalidByteQuantityError) + end + end + end + end +end