solutious-stella 0.6.0 → 0.7.0.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/CHANGES.txt +3 -15
  2. data/LICENSE.txt +1 -1
  3. data/README.rdoc +90 -60
  4. data/Rakefile +32 -42
  5. data/bin/stella +138 -0
  6. data/examples/basic/listing_ids.csv +7 -0
  7. data/examples/basic/plan.rb +71 -0
  8. data/lib/stella.rb +57 -104
  9. data/lib/stella/cli.rb +66 -0
  10. data/lib/stella/client.rb +197 -0
  11. data/lib/stella/config.rb +87 -0
  12. data/lib/stella/data.rb +85 -0
  13. data/lib/stella/data/http.rb +2 -257
  14. data/lib/stella/data/http/body.rb +15 -0
  15. data/lib/stella/data/http/request.rb +116 -0
  16. data/lib/stella/data/http/response.rb +92 -0
  17. data/lib/stella/dsl.rb +5 -0
  18. data/lib/stella/engine.rb +55 -0
  19. data/lib/stella/engine/functional.rb +39 -0
  20. data/lib/stella/engine/load.rb +106 -0
  21. data/lib/stella/exceptions.rb +15 -0
  22. data/lib/stella/guidelines.rb +18 -0
  23. data/lib/stella/mixins.rb +2 -0
  24. data/lib/stella/stats.rb +3 -7
  25. data/lib/stella/testplan.rb +95 -220
  26. data/lib/stella/testplan/stats.rb +26 -0
  27. data/lib/stella/testplan/usecase.rb +67 -0
  28. data/lib/stella/utils.rb +126 -0
  29. data/lib/{util → stella/utils}/httputil.rb +0 -0
  30. data/lib/stella/version.rb +15 -0
  31. data/lib/threadify.rb +0 -6
  32. data/stella.gemspec +43 -49
  33. data/support/example_webapp.rb +246 -0
  34. data/support/useragents.txt +75 -0
  35. metadata +66 -31
  36. data/bin/example_test.rb +0 -82
  37. data/bin/example_webapp.rb +0 -63
  38. data/lib/logger.rb +0 -79
  39. data/lib/stella/clients.rb +0 -161
  40. data/lib/stella/command/base.rb +0 -20
  41. data/lib/stella/command/form.rb +0 -36
  42. data/lib/stella/command/get.rb +0 -44
  43. data/lib/stella/common.rb +0 -53
  44. data/lib/stella/crypto.rb +0 -88
  45. data/lib/stella/data/domain.rb +0 -82
  46. data/lib/stella/environment.rb +0 -66
  47. data/lib/stella/functest.rb +0 -105
  48. data/lib/stella/loadtest.rb +0 -186
  49. data/lib/stella/testrunner.rb +0 -64
  50. data/lib/storable.rb +0 -280
  51. data/lib/timeunits.rb +0 -65
  52. data/tryouts/drb/drb_test.rb +0 -65
  53. data/tryouts/drb/open4.rb +0 -19
  54. data/tryouts/drb/slave.rb +0 -27
  55. data/tryouts/oo_tryout.rb +0 -30
@@ -1,66 +0,0 @@
1
- module Stella
2
- class Environment
3
-
4
-
5
- end
6
- end
7
-
8
- module Stella
9
- class Environment
10
-
11
- attr_accessor :name
12
-
13
- # An array of `Stella::Common::Machine objects to be use during the test.
14
- # @stella_environments.machines << "stellaaahhhh.com:80"
15
- attr_accessor :machines
16
- # The default proxy, a Stella::Common::Proxy object containing the proxy to be used for the test.
17
- attr_accessor :proxy
18
-
19
- def initialize(name=:development)
20
- @name = name
21
- @machines = []
22
- end
23
-
24
-
25
- def add_machines(*args)
26
- return if args.empty?
27
- args.each do |machine|
28
- @machines << Stella::Common::Machine.new(machine)
29
- end
30
- end
31
-
32
- # Creates a Stella::TestPlan::Proxy object and stores it to +@proxy+
33
- def proxy=(*args)
34
- uri, user, pass = args.flatten
35
- @proxy = Stella::Common::Proxy.new(uri, user, pass)
36
- end
37
-
38
-
39
- end
40
- end
41
-
42
- module Stella
43
- module DSL
44
- module Environment
45
- attr_accessor :stella_current_environment
46
-
47
- def environments
48
- @stella_environments
49
- end
50
-
51
- def environment(name, &define)
52
- @stella_environments ||= {}
53
- @stella_current_environment = @stella_environments[name] = Stella::Environment.new(name)
54
- define.call if define
55
- end
56
-
57
- def machines(*args)
58
- return unless @stella_current_environment.is_a? Stella::Environment
59
- args.each do |machine|
60
- @stella_current_environment.add_machines machine
61
- end
62
- end
63
-
64
- end
65
- end
66
- end
@@ -1,105 +0,0 @@
1
-
2
-
3
-
4
- module Stella
5
- class FunctionalTest
6
- include TestRunner
7
-
8
- def type; "functional"; end
9
-
10
-
11
-
12
- def update_start(machine, name)
13
- puts '-'*60
14
- puts "%10s: %s" % ["MACHINE", machine]
15
- end
16
-
17
- def update_authorized(domain, user, pass)
18
- note = user
19
- note += ":****" if pass
20
- puts "%10s: %s" % ["user", note]
21
- puts
22
- end
23
-
24
- def update_request(method, uri, query, response_status, response_headers, response_body)
25
- puts "#{method} #{uri}"
26
- puts "%18s: %s" % ["status", response_status]
27
- puts "%18s: %s" % ["query", query] if @verbose > 0 && !query.empty?
28
- puts "%18s: %s" % ["response_headers", response_headers]
29
- puts "%18s: #{$/}%s" % ["response_body", response_body[0..100]] if @verbose > 0
30
- puts
31
- end
32
-
33
- def update_request_exception(method, uri, query, ex)
34
- puts "#{method} #{uri}"
35
- puts "EXCEPTION: #{ex.message}"
36
- puts ex.backtrace
37
- end
38
-
39
- def update_request_unexpected_response(method, uri, query, response_status, response_headers, response_body)
40
- puts "#{method} #{uri}"
41
- puts "%18s: %s" % ["status", response_status]
42
- puts "%18s: %s" % ["note", "unexpected response status"]
43
- puts "", response_body[0..500]
44
- puts '...' if response_body.length >= 500
45
- end
46
-
47
- def update_retrying(uri, retry_count, total)
48
- puts "retrying: #{uri} (#{retry_count} of #{total})"
49
- end
50
-
51
-
52
- # +environment+ is a Stella::Common::Environment object.
53
- # +namespace+ is a reference to the namespace which contains the instance
54
- # variables. This will be the section of code that makes use of the DSL.
55
- def run(environment, namespace)
56
- raise "No testplan defined" unless @testplan
57
- raise "No machines defined for #{environment.name}" if environment.machines.empty?
58
-
59
-
60
- begin
61
- if environment.proxy
62
- http_client = HTTPClient.new(environment.proxy.uri)
63
- http_client.set_proxy_auth(environment.proxy.user, environment.proxy.pass) if environment.proxy.user
64
- else
65
- http_client = HTTPClient.new
66
- end
67
- rescue => ex
68
- puts ex.class
69
- end
70
-
71
- request_stats = {}
72
- environment.machines.each do |machine|
73
- client = Stella::Client.new
74
- client.add_observer(self)
75
- client.execute_testplan(request_stats, http_client, machine, namespace, @testplan, @verbose)
76
- end
77
-
78
-
79
- request_stats.each do |rstat|
80
- puts rstat[1][:stats].to_s
81
- end
82
- end
83
-
84
-
85
- end
86
- end
87
-
88
-
89
-
90
-
91
- module Stella
92
- module DSL
93
- module FunctionalTest
94
- include Stella::DSL::TestRunner
95
-
96
- def functest(name=:default, &define)
97
- @tests ||= {}
98
- @current_test = @tests[name] = Stella::FunctionalTest.new(name)
99
- define.call if define
100
- end
101
-
102
-
103
- end
104
- end
105
- end
@@ -1,186 +0,0 @@
1
- # See, re Threadify on JRuby: http://www.ruby-forum.com/topic/158180
2
-
3
- #
4
- #
5
- module Stella
6
- class LoadTest
7
- include TestRunner
8
-
9
- attr_accessor :clients
10
- attr_accessor :repetitions
11
- attr_accessor :duration
12
-
13
- attr_reader :testplans_started
14
- attr_reader :testplans_completed
15
-
16
- attr_reader :requests_successful
17
- attr_reader :requests_failed
18
-
19
- def init
20
- @repetitions = 1
21
- @clients = 1
22
- @duration = 0
23
- reset
24
- end
25
-
26
- def reset
27
- @testplans_started = 0
28
- @testplans_completed = 0
29
- @requests_successful = 0
30
- @requests_failed = 0
31
- end
32
-
33
- def type; "load"; end
34
-
35
- def requests_total
36
- @requests_successful + @requests_failed
37
- end
38
-
39
- def update_start(machine, name)
40
- @testplans_started += 1
41
- end
42
-
43
- def update_done(*args)
44
- @testplans_completed += 1
45
- end
46
-
47
- def update_authorized(domain, user, pass)
48
- end
49
-
50
- def update_request(method, uri, query, response_status, response_headers, response_body)
51
- @requests_successful += 1
52
- end
53
-
54
- def update_request_exception(method, uri, query, ex)
55
- @requests_failed += 1
56
- puts [method, uri, query, ex.message].join("|")
57
- end
58
-
59
- def update_request_unexpected_response(method, uri, query, response_status, response_headers, response_body)
60
- @requests_failed += 1
61
- #puts [method, uri, query, response_status, response_headers, response_body].join("|")
62
- end
63
-
64
- def update_retrying(uri, retry_count, total)
65
- #print retry_count
66
- end
67
-
68
-
69
- # +environment+ is a Stella::Common::Environment object.
70
- # +namespace+ is a reference to the namespace which contains the instance
71
- # variables.
72
- def run(environment, namespace)
73
- raise "No testplan defined" unless @testplan
74
- raise "No machines defined for #{environment.name}" if environment.machines.empty?
75
-
76
- [:duration, :clients, :repetitions].each do |p|
77
- val = instance_variable_get("@#{p}")
78
- puts " %11s: %s" % [p, val] if val
79
- end
80
-
81
- stats = Stats.new("LoadTest")
82
- request_stats = {}
83
-
84
- time_started = Time.now
85
- seconds_elapsed = 0
86
- (1..@clients).to_a.threadify do |i|
87
-
88
- (0..@repetitions).to_a.each do |rep|
89
-
90
- if environment.proxy
91
- http_client = HTTPClient.new(environment.proxy.uri)
92
- http_client.set_proxy_auth(environment.proxy.user, environment.proxy.pass) if environment.proxy.user
93
- else
94
- http_client = HTTPClient.new
95
- end
96
-
97
-
98
- environment.machines.each do |machine|
99
- client = Stella::Client.new(i)
100
- client.add_observer(self)
101
- client.execute_testplan(request_stats, http_client, machine, namespace, @testplan, @verbose)
102
- end
103
-
104
- seconds_elapsed = Time.now - time_started
105
-
106
- #request_stats.each do |rstat|
107
- # puts rstat[1][:stats].to_s
108
- #end
109
-
110
- # If a duration was given, we make sure to run for that
111
- # amount of time.
112
- if @duration > 0
113
- redo if seconds_elapsed <= @duration
114
- break if seconds_elapsed >= @duration
115
- end
116
- end
117
- end
118
-
119
- stats.tick
120
- puts "DONE! (#{seconds_elapsed.minutes} minutes)"
121
- instance_variables.each do |name|
122
- #next unless name =~ /request/
123
- puts "%20s: %s" % [name, instance_variable_get(name)]
124
- end
125
-
126
- puts "Final Status"
127
- puts stats.to_s
128
- puts
129
-
130
- request_stats.each do |rstat|
131
- puts rstat[1][:stats].to_s
132
- end
133
- end
134
-
135
-
136
- def clients=(*args)
137
- count = args.flatten.first
138
- @clients = count
139
- end
140
-
141
- def repetitions=(*args)
142
- @repetitions = args.flatten.first
143
- end
144
-
145
- def duration=(*args)
146
- @duration = args.flatten.first
147
- end
148
- end
149
- end
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
- module Stella
158
- module DSL
159
- module LoadTest
160
- include Stella::DSL::TestRunner
161
-
162
- def loadtest(name=:default, &define)
163
- @tests ||= {}
164
- @current_test = @tests[name] = Stella::LoadTest.new(name)
165
- define.call if define
166
- end
167
-
168
- def rampup(*args)
169
- end
170
-
171
- def warmup(*args)
172
- end
173
-
174
- [:repetitions, :duration, :clients].each do |method_name|
175
- eval <<-RUBY, binding, '(Stella::DSL::LoadTest)', 1
176
- def #{method_name}(*val)
177
- return unless @current_test.is_a? Stella::LoadTest
178
- @current_test.#{method_name}=(val)
179
- end
180
- private :#{method_name}
181
- RUBY
182
- end
183
-
184
- end
185
- end
186
- end
@@ -1,64 +0,0 @@
1
- # ---
2
- # See: http://codeforpeople.com/lib/ruby/flow/flow-2.0.0/sample/a.rb
3
- # +++
4
-
5
- #
6
- #
7
- #
8
- module Stella
9
- module TestRunner
10
- attr_accessor :name
11
- # Name or instance of the testplan to execute
12
- attr_accessor :testplan
13
- # Determines the amount of output. Default: 0
14
- attr_accessor :verbose
15
-
16
- def initialize(name=:default)
17
- @name = name
18
- @verbose = 0
19
- init if respond_to? :init
20
- end
21
-
22
- def update(*args)
23
- what, *args = args
24
- self.send("update_#{what}", *args) if respond_to? "update_#{what}"
25
- end
26
-
27
- end
28
- module DSL
29
- module TestRunner
30
- attr_accessor :current_test
31
-
32
- def plan(testplan)
33
- raise "Unknown testplan, '#{testplan}'" unless @plans.has_key?(testplan)
34
- return unless @current_test
35
- @current_test.testplan = @plans[testplan]
36
- end
37
-
38
- def run(env_name=nil, test_name=nil)
39
- to_run = test_name.nil? ? @tests : [@tests[test_name]]
40
- env = env_name.nil? ? @stella_environments.first : @stella_environments[env_name]
41
- to_run.each do |t|
42
- puts '='*60
43
- puts "RUNNING TEST: #{test_name}"
44
- puts " %11s: %s" % ['type', t.type]
45
- puts " %11s: %s" % ['testplan', t.testplan.name]
46
- puts " %11s: %s" % ['desc', t.testplan.description]
47
- puts " %11s: %s" % ['env', env_name]
48
-
49
-
50
- t.run(env, self)
51
- end
52
- end
53
-
54
- def verbose(*args)
55
- @current_test.verbose += args.first || 1
56
- end
57
-
58
- private
59
-
60
- end
61
- end
62
- end
63
-
64
-
@@ -1,280 +0,0 @@
1
- #--
2
- # TODO: Handle nested hashes and arrays.
3
- # TODO: to_xml, see: http://codeforpeople.com/lib/ruby/xx/xx-2.0.0/README
4
- # TODO: Rename to Stuffany
5
- #++
6
-
7
- require 'yaml'
8
- require 'fileutils'
9
-
10
-
11
- # Storable makes data available in multiple formats and can
12
- # re-create objects from files. Fields are defined using the
13
- # Storable.field method which tells Storable the order and
14
- # name.
15
- class Storable
16
- VERSION = 2
17
- NICE_TIME_FORMAT = "%Y-%m-%d@%H:%M:%S".freeze unless defined? NICE_TIME_FORMAT
18
- SUPPORTED_FORMATS = %w{tsv csv yaml json}.freeze unless defined? SUPPORTED_FORMATS
19
-
20
- # This value will be used as a default unless provided on-the-fly.
21
- # See SUPPORTED_FORMATS for available values.
22
- attr_reader :format
23
-
24
- # See SUPPORTED_FORMATS for available values
25
- def format=(v)
26
- raise "Unsupported format: #{v}" unless SUPPORTED_FORMATS.member?(v)
27
- @format = v
28
- end
29
-
30
- # TODO: from_args([HASH or ordered params])
31
-
32
- def init
33
- # NOTE: I think this can be removed
34
- self.class.send(:class_variable_set, :@@field_names, []) unless class_variable_defined?(:@@field_names)
35
- self.class.send(:class_variable_set, :@@field_types, []) unless class_variable_defined?(:@@field_types)
36
- end
37
-
38
- # Accepts field definitions in the one of the follow formats:
39
- #
40
- # field :product
41
- # field :product => Integer
42
- #
43
- # The order they're defined determines the order the will be output. The fields
44
- # data is available by the standard accessors, class.product and class.product= etc...
45
- # The value of the field will be cast to the type (if provided) when read from a file.
46
- # The value is not touched when the type is not provided.
47
- def self.field(args={})
48
- # TODO: Examine casting from: http://codeforpeople.com/lib/ruby/fattr/fattr-1.0.3/
49
- args = {args => nil} unless args.is_a? Hash
50
-
51
- args.each_pair do |m,t|
52
-
53
- [[:@@field_names, m], [:@@field_types, t]].each do |tuple|
54
- class_variable_set(tuple[0], []) unless class_variable_defined?(tuple[0])
55
- class_variable_set(tuple[0], class_variable_get(tuple[0]) << tuple[1])
56
- end
57
-
58
- next if method_defined?(m)
59
-
60
- define_method(m) do instance_variable_get("@#{m}") end
61
- define_method("#{m}=") do |val|
62
- instance_variable_set("@#{m}",val)
63
- end
64
- end
65
- end
66
-
67
- # Returns an array of field names defined by self.field
68
- def self.field_names
69
- class_variable_get(:@@field_names)
70
- end
71
- # Ditto.
72
- def field_names
73
- self.class.send(:class_variable_get, :@@field_names)
74
- end
75
- # Returns an array of field types defined by self.field. Fields that did
76
- # not receive a type are set to nil.
77
- def self.field_types
78
- class_variable_get(:@@field_types)
79
- end
80
- # Ditto.
81
- def field_types
82
- self.class.send(:class_variable_get, :@@field_types)
83
- end
84
-
85
- # Dump the object data to the given format.
86
- def dump(format=nil, with_titles=true)
87
- format ||= @format
88
- raise "Format not defined (#{format})" unless SUPPORTED_FORMATS.member?(format)
89
- send("to_#{format}", with_titles)
90
- end
91
-
92
- # Create a new instance of the object using data from file.
93
- def self.from_file(file_path, format='yaml')
94
- raise "Cannot read file (#{file_path})" unless File.exists?(file_path)
95
- raise "#{self} doesn't support from_#{format}" unless self.respond_to?("from_#{format}")
96
- format = format || File.extname(file_path).tr('.', '')
97
- me = send("from_#{format}", read_file_to_array(file_path))
98
- me.format = format
99
- me
100
- end
101
- # Write the object data to the given file.
102
- def to_file(file_path=nil, with_titles=true)
103
- raise "Cannot store to nil path" if file_path.nil?
104
- format = File.extname(file_path).tr('.', '')
105
- format ||= @format
106
- Storable.write_file(file_path, dump(format, with_titles))
107
- end
108
-
109
- # Create a new instance of the object from a hash.
110
- def self.from_hash(from={})
111
- me = self.new
112
-
113
- return me if !from || from.empty?
114
-
115
- fnames = field_names
116
- fnames.each_with_index do |key,index|
117
-
118
- stored_value = from[key] || from[key.to_s] # support for symbol keys and string keys
119
-
120
- # TODO: Correct this horrible implementation (sorry, me. It's just one of those days.)
121
-
122
- if field_types[index] == Array
123
- ((value ||= []) << stored_value).flatten
124
- elsif field_types[index] == Hash
125
- value = stored_value
126
- else
127
- # SimpleDB stores attribute shit as lists of values
128
- value = stored_value.first if stored_value.is_a?(Array) && stored_value.size == 1
129
-
130
- if field_types[index] == Time
131
- value = Time.parse(value)
132
- elsif field_types[index] == DateTime
133
- value = DateTime.parse(value)
134
- elsif field_types[index] == TrueClass
135
- value = (value.to_s == "true")
136
- elsif field_types[index] == Float
137
- value = value.to_f
138
- elsif field_types[index] == Integer
139
- value = value.to_i
140
- else
141
- value = value.first if value.is_a?(Array) && value.size == 1 # I
142
- end
143
- end
144
-
145
- me.send("#{key}=", value) if self.method_defined?("#{key}=")
146
- end
147
-
148
- me
149
- end
150
- # Return the object data as a hash
151
- # +with_titles+ is ignored.
152
- def to_hash(with_titles=true)
153
- tmp = {}
154
- field_names.each do |fname|
155
- tmp[fname] = self.send(fname)
156
- end
157
- tmp
158
- end
159
-
160
- # Create a new instance of the object from YAML.
161
- # +from+ a YAML string split into an array by line.
162
- def self.from_yaml(from=[])
163
- # from is an array of strings
164
- from_str = from.join('')
165
- hash = YAML::load(from_str)
166
- hash = from_hash(hash) if hash.is_a? Hash
167
- hash
168
- end
169
- def to_yaml(with_titles=true)
170
- to_hash.to_yaml
171
- end
172
-
173
- # Create a new instance of the object from a JSON string.
174
- # +from+ a JSON string split into an array by line.
175
- def self.from_json(from=[])
176
- require 'json'
177
- # from is an array of strings
178
- from_str = from.join('')
179
- tmp = JSON::load(from_str)
180
- hash_sym = tmp.keys.inject({}) do |hash, key|
181
- hash[key.to_sym] = tmp[key]
182
- hash
183
- end
184
- hash_sym = from_hash(hash_sym) if hash_sym.is_a? Hash
185
- hash_sym
186
- end
187
- def to_json(with_titles=true)
188
- require 'json'
189
- to_hash.to_json
190
- end
191
-
192
- # Return the object data as a delimited string.
193
- # +with_titles+ specifiy whether to include field names (default: false)
194
- # +delim+ is the field delimiter.
195
- def to_delimited(with_titles=false, delim=',')
196
- values = []
197
- field_names.each do |fname|
198
- values << self.send(fname.to_s) # TODO: escape values
199
- end
200
- output = values.join(delim)
201
- output = field_names.join(delim) << $/ << output if with_titles
202
- output
203
- end
204
- # Return the object data as a tab delimited string.
205
- # +with_titles+ specifiy whether to include field names (default: false)
206
- def to_tsv(with_titles=false)
207
- to_delimited(with_titles, "\t")
208
- end
209
- # Return the object data as a comma delimited string.
210
- # +with_titles+ specifiy whether to include field names (default: false)
211
- def to_csv(with_titles=false)
212
- to_delimited(with_titles, ',')
213
- end
214
- # Create a new instance from tab-delimited data.
215
- # +from+ a JSON string split into an array by line.
216
- def self.from_tsv(from=[])
217
- self.from_delimited(from, "\t")
218
- end
219
- # Create a new instance of the object from comma-delimited data.
220
- # +from+ a JSON string split into an array by line.
221
- def self.from_csv(from=[])
222
- self.from_delimited(from, ',')
223
- end
224
-
225
- # Create a new instance of the object from a delimited string.
226
- # +from+ a JSON string split into an array by line.
227
- # +delim+ is the field delimiter.
228
- def self.from_delimited(from=[],delim=',')
229
- return if from.empty?
230
- # We grab an instance of the class so we can
231
- hash = {}
232
-
233
- fnames = values = []
234
- if (from.size > 1 && !from[1].empty?)
235
- fnames = from[0].chomp.split(delim)
236
- values = from[1].chomp.split(delim)
237
- else
238
- fnames = self.field_names
239
- values = from[0].chomp.split(delim)
240
- end
241
-
242
- fnames.each_with_index do |key,index|
243
- next unless values[index]
244
- hash[key.to_sym] = values[index]
245
- end
246
- hash = from_hash(hash) if hash.is_a? Hash
247
- hash
248
- end
249
-
250
- def self.read_file_to_array(path)
251
- contents = []
252
- return contents unless File.exists?(path)
253
-
254
- open(path, 'r') do |l|
255
- contents = l.readlines
256
- end
257
-
258
- contents
259
- end
260
-
261
- def self.write_file(path, content, flush=true)
262
- write_or_append_file('w', path, content, flush)
263
- end
264
-
265
- def self.append_file(path, content, flush=true)
266
- write_or_append_file('a', path, content, flush)
267
- end
268
-
269
- def self.write_or_append_file(write_or_append, path, content = '', flush = true)
270
- #STDERR.puts "Writing to #{ path }..."
271
- create_dir(File.dirname(path))
272
-
273
- open(path, write_or_append) do |f|
274
- f.puts content
275
- f.flush if flush;
276
- end
277
- File.chmod(0600, path)
278
- end
279
- end
280
-