sugar_utils 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6fa1916d16c20d95a15c4efe888721b1ba36a526
4
- data.tar.gz: ca2efdbff746c027cf648c2c6ea362283b44482c
3
+ metadata.gz: c614940d4ed952e41a7b52798188e3a0ac39a983
4
+ data.tar.gz: f5ddaf4709df842179c3d85f1b4abdf67c39c636
5
5
  SHA512:
6
- metadata.gz: 51daa978771f007d6e9bd816ee1d7a5652e348446627212c54de0f2a1476b7a0100e22fb9ed95a69cb04920433a3eea49bb9ecf096b59702ed9ce8f1de04dd6c
7
- data.tar.gz: 9fc27c24a3bc54294499ace731d3b649b51d5ffdff9499e94f1a0d7a387e2957d92686ce10caca6d5874c968de18710f76a9ccceff344f9e5b2519f0e29b4134
6
+ metadata.gz: e09c44845626a114fbacc8a9a8ba82db43abf86d40951eaf4e880d1779469459407cc020c6861a278dbee23f2cbe1cae8f0bbcb64f137e97965b51dc913f3332
7
+ data.tar.gz: 64f9577e97eafbd53f4b9f8c2a8d8bdd114677bfbd2b73c45e9564bbc5245f9a665271f4bc32db4829094e74f9a12ffcc9b07033bf8f331c002ee2b566e10523
data/.travis.yml CHANGED
@@ -1,3 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.0.0
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ - ruby-head
7
+ matrix:
8
+ allow_failures:
9
+ - rvm: ruby-head
10
+ sudo: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ## [0.2.0] - 2016-07-21
6
+ ### Added
7
+ - SugarUtils::File.flock, for file locking with a timeout
8
+ - SugarUtils::File.read_json, with locking and error handling
9
+ - SugarUtils::File.write_json, with locking and error handling
10
+ - this CHANGELOG.md, following the http://keepachangelog.com/ guidelines
11
+
12
+ ## [0.1.0] - 2016-07-09
13
+ ### Added
14
+ - SugarUtils.ensure_boolean
15
+ - SugarUtils.ensure_integer
@@ -0,0 +1,91 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'solid_assert'
4
+ require 'fileutils'
5
+ require 'multi_json'
6
+
7
+ module SugarUtils
8
+ module File
9
+ class Error < StandardError; end
10
+
11
+ # flock with a timeout to ensure that it does not flock forever.
12
+ #
13
+ # @see http://www.codegnome.com/blog/2013/05/26/locking-files-with-ruby/
14
+ #
15
+ # @param [File] file
16
+ # @param [File::LOCK_EX, File::LOCK_SH] locking_constant
17
+ # @param [Hash] options
18
+ # @option options [Integer] :timeout (10)
19
+ #
20
+ # @return [void]
21
+ def self.flock(file, locking_constant, options = {})
22
+ timeout = options[:timeout] || 10
23
+ Timeout.timeout(timeout) { file.flock(locking_constant) }
24
+ end
25
+
26
+ # @param [String] filename
27
+ # @param [Hash] options
28
+ # @option options [Integer] :timeout (10)
29
+ # @option options [Boolean] :raise_on_missing (true)
30
+ #
31
+ # @raise [SugarUtils::File::Error)
32
+ #
33
+ # @return [Object]
34
+ def self.read_json(filename, options = {})
35
+ assert(options.is_a?(Hash), 'options MUST be a Hash')
36
+
37
+ options[:raise_on_missing] = true if options[:raise_on_missing].nil?
38
+
39
+ ::File.open(filename, ::File::RDONLY) do |file|
40
+ flock(file, ::File::LOCK_SH, options)
41
+ MultiJson.load(file.read)
42
+ end
43
+ rescue SystemCallError, IOError
44
+ raise(Error, "Cannot read #{filename}") if options[:raise_on_missing]
45
+ {}
46
+ rescue MultiJson::ParseError
47
+ raise(Error, "Cannot parse #{filename}")
48
+ rescue Timeout::Error
49
+ raise(Error, "Cannot read #{filename} because it is locked")
50
+ end
51
+
52
+ # @param [String] filename
53
+ # @param [#to_json] data
54
+ # @param [Hash] options
55
+ # @option options [Integer] :timeout (10)
56
+ # @option options [Boolean] :flush (false)
57
+ # @option options [Integer] :perm (0666)
58
+ #
59
+ # @raise [SugarUtils::File::Error)
60
+ #
61
+ # @return [void]
62
+ def self.write_json(filename, data, options = {})
63
+ perm = options[:perm] || 0666
64
+ flush = options[:flush] || false
65
+
66
+ FileUtils.mkdir_p(::File.dirname(filename))
67
+ ::File.open(filename, ::File::RDWR | ::File::CREAT, perm) do |file|
68
+ flock(file, ::File::LOCK_EX, options)
69
+
70
+ file.truncate(0) # Ensure file is empty before proceeding.
71
+ file.puts(MultiJson.dump(data, pretty: true))
72
+
73
+ # Flush and fsync to be 100% sure we write this data out now because we
74
+ # are often reading it immediately and if the OS is buffering, it is
75
+ # possible we might read it before it is been physically written to disk.
76
+ # We are not worried about speed here, so this should be OKAY.
77
+ if flush
78
+ file.flush
79
+ file.fsync
80
+ end
81
+
82
+ # Ensure that the permissions are correct if the file already existed.
83
+ file.chmod(perm)
84
+ end
85
+ rescue Timeout::Error
86
+ raise(Error, "Unable to write #{filename} because it is locked")
87
+ rescue SystemCallError, IOError => boom
88
+ raise(Error, "Unable to write #{filename} with #{boom}")
89
+ end
90
+ end
91
+ end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  module SugarUtils
4
- VERSION = '0.1.0'.freeze
4
+ VERSION = '0.2.0'.freeze
5
5
  end
data/lib/sugar_utils.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  require 'sugar_utils/version'
4
+ require 'sugar_utils/file'
4
5
 
5
6
  module SugarUtils
6
7
  # @param [Object] value
data/spec/spec_helper.rb CHANGED
@@ -4,4 +4,11 @@ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
4
  require 'sugar_utils'
5
5
  require 'simplecov'
6
6
  require 'rspec/tabular'
7
+ require 'fakefs/spec_helpers'
7
8
  SimpleCov.start 'rails'
9
+
10
+ SolidAssert.enable_assertions
11
+
12
+ RSpec.configure do |config|
13
+ config.include FakeFS::SpecHelpers, fakefs: true
14
+ end
@@ -0,0 +1,168 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+
6
+ describe SugarUtils::File do
7
+ describe '.flock' do
8
+ subject { described_class.flock(file, :locking_constant, options) }
9
+ let(:file) { instance_double(File) }
10
+ before do
11
+ expect(Timeout).to receive(:timeout).with(expected_timeout).and_yield
12
+ expect(file).to receive(:flock).with(:locking_constant)
13
+ end
14
+
15
+ inputs :options, :expected_timeout
16
+ side_effects_with Hash[], 10
17
+ side_effects_with Hash[timeout: nil], 10
18
+ side_effects_with Hash[timeout: 5], 5
19
+ end
20
+
21
+ describe '.read_json', :fakefs do
22
+ subject { described_class.read_json('filename.json', options) }
23
+
24
+ context 'missing file' do
25
+ inputs :options
26
+ raise_error_with Hash[], described_class::Error
27
+ raise_error_with Hash[], 'Cannot read filename.json'
28
+ raise_error_with Hash[raise_on_missing: true], described_class::Error
29
+ raise_error_with Hash[raise_on_missing: true], 'Cannot read filename.json'
30
+ it_with Hash[raise_on_missing: false], {}
31
+ end
32
+
33
+ context 'file present' do
34
+ let(:options) { {} }
35
+ before { write('filename.json', content) }
36
+
37
+ context 'SysteCallError' do
38
+ let(:options) { {} }
39
+ let(:content) { '' }
40
+ let(:exception) { SystemCallError.new(nil) }
41
+ before { allow(File).to receive(:open).and_raise(exception) }
42
+ it { expect_raise_error('Cannot read filename.json') }
43
+ end
44
+
45
+ context 'IOError' do
46
+ let(:options) { {} }
47
+ let(:content) { '' }
48
+ let(:exception) { IOError.new(nil) }
49
+ before { allow(File).to receive(:open).and_raise(exception) }
50
+ it { expect_raise_error('Cannot read filename.json') }
51
+ end
52
+
53
+ context 'and locked' do
54
+ let(:content) { '' }
55
+ before { expect_flock(File::LOCK_SH, options).and_raise(Timeout::Error) }
56
+ it { expect_raise_error('Cannot read filename.json because it is locked') }
57
+ end
58
+
59
+ context 'and unlocked' do
60
+ before { expect_flock(File::LOCK_SH, options) }
61
+
62
+ inputs :content
63
+ raise_error_with 'I am not json', described_class::Error
64
+ raise_error_with 'I am not json', 'Cannot parse filename.json'
65
+ it_with Hash['key' => 'value'].to_json, Hash['key' => 'value']
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '.write_json', :fakefs do
71
+ subject { described_class.write_json(filename, data, options) }
72
+ let(:data) { { 'key' => 'value' } }
73
+ let(:filename) { 'dir1/dir2/filename.json' }
74
+
75
+ context 'SystemCallError' do
76
+ let(:options) { {} }
77
+ let(:exception) { SystemCallError.new(nil) }
78
+ before { allow(File).to receive(:open).and_raise(exception) }
79
+ it { expect_raise_error("Unable to write #{filename} with #{exception}") }
80
+ end
81
+
82
+ context 'IOError' do
83
+ let(:options) { {} }
84
+ let(:exception) { IOError.new(nil) }
85
+ before { allow(File).to receive(:open).and_raise(exception) }
86
+ it { expect_raise_error("Unable to write #{filename} with #{exception}") }
87
+ end
88
+
89
+ context 'locked' do
90
+ let(:options) { {} }
91
+ before { expect_flock(File::LOCK_EX, options).and_raise(Timeout::Error) }
92
+ it { expect_raise_error("Unable to write #{filename} because it is locked") }
93
+ end
94
+
95
+ context 'unlocked' do
96
+ shared_examples_for 'file is written' do
97
+ before { expect_flock(File::LOCK_EX, options) }
98
+
99
+ context 'default options' do
100
+ let(:options) { {} }
101
+ before { subject }
102
+ specify do
103
+ expect(File.exist?(filename)).to eq(true)
104
+ expect(MultiJson.load(File.read(filename))).to eq(data)
105
+ expect(sprintf('%o', File.stat(filename).mode)).to eq('100666')
106
+ end
107
+ end
108
+
109
+ context 'options' do
110
+ let(:options) { { flush: true, perm: 0600 } }
111
+ before do
112
+ expect_any_instance_of(File).to receive(:flush)
113
+ expect_any_instance_of(File).to receive(:fsync)
114
+ subject
115
+ end
116
+ specify do
117
+ expect(File.exist?(filename)).to eq(true)
118
+ expect(MultiJson.load(File.read(filename))).to eq(data)
119
+ expect(sprintf('%o', File.stat(filename).mode)).to eq('100600')
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'and not exist' do
125
+ it_behaves_like 'file is written'
126
+ end
127
+
128
+ context 'and exists' do
129
+ before { write(filename, 'foobar', 0777) }
130
+ context 'not locked' do
131
+ it_behaves_like 'file is written'
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ ##############################################################################
138
+
139
+ # @param [File::LOCK_SH, File::LOCK_EX] locking_constant
140
+ # @param [Hash] options
141
+ def expect_flock(locking_constant, options)
142
+ expect(described_class).to receive(:flock)
143
+ .with(kind_of(File), locking_constant, options)
144
+ end
145
+
146
+ # @param [String] message
147
+ def expect_raise_error(message)
148
+ expect { subject }.to raise_error(described_class::Error, message)
149
+ end
150
+
151
+ # @overload write(filename, content)
152
+ # @param [String] filename
153
+ # @param [String] content
154
+ #
155
+ # @overload write(filename, content, perm)
156
+ # @param [String] filename
157
+ # @param [String] content
158
+ # @param [Integer] perm
159
+ #
160
+ # @return [void]
161
+ def write(filename, content, perm = nil)
162
+ FileUtils.mkdir_p(::File.dirname(filename))
163
+ File.write(filename, content)
164
+ FileUtils.chmod(perm, filename) if perm
165
+ end
166
+
167
+
168
+ end
data/sugar_utils.gemspec CHANGED
@@ -17,7 +17,10 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ['lib']
19
19
 
20
- spec.add_development_dependency 'bundler', '~> 1.9'
20
+ spec.add_dependency 'multi_json', '~> 1.12.1'
21
+ spec.add_dependency 'solid_assert', '~> 1.0'
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.7'
21
24
  spec.add_development_dependency 'rake', '~> 10.0'
22
25
  spec.add_development_dependency 'rspec', '~> 3.4.0'
23
26
  spec.add_development_dependency 'rspec-tabular', '~> 0.1.0'
@@ -25,4 +28,5 @@ Gem::Specification.new do |spec|
25
28
  spec.add_development_dependency 'rubocop', '~> 0.37'
26
29
  spec.add_development_dependency 'yard', '~> 0.8.7.6'
27
30
  spec.add_development_dependency 'yardstick', '~> 0.9.9'
31
+ spec.add_development_dependency 'fakefs', '~> 0.7'
28
32
  end
metadata CHANGED
@@ -1,29 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sugar_utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Sullivan Cant
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-09 00:00:00.000000000 Z
11
+ date: 2016-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: multi_json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.12.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.12.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: solid_assert
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
16
44
  requirements:
17
45
  - - ~>
18
46
  - !ruby/object:Gem::Version
19
- version: '1.9'
47
+ version: '1.7'
20
48
  type: :development
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
52
  - - ~>
25
53
  - !ruby/object:Gem::Version
26
- version: '1.9'
54
+ version: '1.7'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rake
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +150,20 @@ dependencies:
122
150
  - - ~>
123
151
  - !ruby/object:Gem::Version
124
152
  version: 0.9.9
153
+ - !ruby/object:Gem::Dependency
154
+ name: fakefs
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ~>
158
+ - !ruby/object:Gem::Version
159
+ version: '0.7'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ~>
165
+ - !ruby/object:Gem::Version
166
+ version: '0.7'
125
167
  description:
126
168
  email:
127
169
  - acant@sugarcrm.com
@@ -133,6 +175,7 @@ files:
133
175
  - .rspec
134
176
  - .rubocop.yml
135
177
  - .travis.yml
178
+ - CHANGELOG.md
136
179
  - CONTRIBUTING.md
137
180
  - CONTRIBUTOR_TERMS.md
138
181
  - Gemfile
@@ -140,8 +183,10 @@ files:
140
183
  - README.md
141
184
  - Rakefile
142
185
  - lib/sugar_utils.rb
186
+ - lib/sugar_utils/file.rb
143
187
  - lib/sugar_utils/version.rb
144
188
  - spec/spec_helper.rb
189
+ - spec/sugar_utils/file_spec.rb
145
190
  - spec/sugar_utils_spec.rb
146
191
  - sugar_utils.gemspec
147
192
  homepage: http://github.com/sugarcrm/sugar_utils
@@ -170,5 +215,6 @@ specification_version: 4
170
215
  summary: Utility methods extracted from SugarCRM Ruby projects.
171
216
  test_files:
172
217
  - spec/spec_helper.rb
218
+ - spec/sugar_utils/file_spec.rb
173
219
  - spec/sugar_utils_spec.rb
174
220
  has_rdoc: