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 +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:
|