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 +4 -4
- data/.travis.yml +8 -1
- data/CHANGELOG.md +15 -0
- data/lib/sugar_utils/file.rb +91 -0
- data/lib/sugar_utils/version.rb +1 -1
- data/lib/sugar_utils.rb +1 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/sugar_utils/file_spec.rb +168 -0
- data/sugar_utils.gemspec +5 -1
- metadata +50 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c614940d4ed952e41a7b52798188e3a0ac39a983
|
4
|
+
data.tar.gz: f5ddaf4709df842179c3d85f1b4abdf67c39c636
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e09c44845626a114fbacc8a9a8ba82db43abf86d40951eaf4e880d1779469459407cc020c6861a278dbee23f2cbe1cae8f0bbcb64f137e97965b51dc913f3332
|
7
|
+
data.tar.gz: 64f9577e97eafbd53f4b9f8c2a8d8bdd114677bfbd2b73c45e9564bbc5245f9a665271f4bc32db4829094e74f9a12ffcc9b07033bf8f331c002ee2b566e10523
|
data/.travis.yml
CHANGED
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
|
data/lib/sugar_utils/version.rb
CHANGED
data/lib/sugar_utils.rb
CHANGED
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.
|
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.
|
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-
|
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.
|
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.
|
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:
|