sugar_utils 0.1.0 → 0.2.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 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: