bookie_accounting 1.2.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -3
  3. data/README.md +4 -24
  4. data/Rakefile +9 -116
  5. data/bin/bookie-data +48 -7
  6. data/bin/bookie-send +6 -14
  7. data/bookie_accounting.gemspec +4 -3
  8. data/lib/bookie/database/group.rb +33 -0
  9. data/lib/bookie/database/job.rb +201 -0
  10. data/lib/bookie/database/job_summary.rb +268 -0
  11. data/lib/bookie/database/lock.rb +36 -0
  12. data/lib/bookie/database/system.rb +166 -0
  13. data/lib/bookie/database/system_type.rb +80 -0
  14. data/lib/bookie/database/user.rb +54 -0
  15. data/lib/bookie/database.rb +7 -805
  16. data/lib/bookie/extensions.rb +23 -44
  17. data/lib/bookie/formatter.rb +8 -4
  18. data/lib/bookie/sender.rb +12 -14
  19. data/lib/bookie/version.rb +1 -1
  20. data/snapshot/test_config.json +2 -2
  21. data/spec/config_spec.rb +2 -2
  22. data/spec/database/group_spec.rb +36 -0
  23. data/spec/database/job_spec.rb +308 -0
  24. data/spec/database/job_summary_spec.rb +302 -0
  25. data/spec/database/lock_spec.rb +41 -0
  26. data/spec/database/migration_spec.rb +44 -0
  27. data/spec/database/system_spec.rb +232 -0
  28. data/spec/database/system_type_spec.rb +68 -0
  29. data/spec/database/user_spec.rb +69 -0
  30. data/spec/formatter_spec.rb +44 -37
  31. data/spec/{comma_dump_formatter_spec.rb → formatters/comma_dump_spec.rb} +16 -30
  32. data/spec/formatters/spreadsheet_spec.rb +98 -0
  33. data/spec/{stdout_formatter_spec.rb → formatters/stdout_spec.rb} +15 -29
  34. data/spec/sender_spec.rb +92 -66
  35. data/spec/{standalone_sender_spec.rb → senders/standalone_spec.rb} +10 -9
  36. data/spec/{torque_cluster_sender_spec.rb → senders/torque_cluster_spec.rb} +9 -13
  37. data/spec/spec_helper.rb +111 -57
  38. data/todo.txt +13 -0
  39. metadata +38 -23
  40. data/rpm/activesupport.erb +0 -151
  41. data/rpm/bundle.erb +0 -71
  42. data/rpm/default.erb +0 -147
  43. data/rpm/mysql2.erb +0 -149
  44. data/rpm/pacct.erb +0 -147
  45. data/rpm/rspec-core.erb +0 -149
  46. data/rpm/sqlite3.erb +0 -147
  47. data/spec/database_spec.rb +0 -1078
  48. data/spec/spreadsheet_formatter_spec.rb +0 -114
@@ -1,3 +1,4 @@
1
+ #TODO: revise unit tests.
1
2
 
2
3
  ##
3
4
  #Reopened to add some useful methods
@@ -6,58 +7,36 @@ class Range
6
7
  #If end < begin, returns an empty range (begin ... begin)
7
8
  #Otherwise, returns the original range
8
9
  def normalized
9
- return self.begin ... self.begin if self.end < self.begin
10
+ if self.end < self.begin
11
+ self.begin ... self.begin
12
+ else
10
13
  self
14
+ end
15
+ end
16
+
17
+ ##
18
+ #Converts the range to an equivalent exclusive range (one where exclude_end? is true)
19
+ #
20
+ #Only works for ranges with discrete steps between values (i.e. integers)
21
+ def exclusive
22
+ if exclude_end?
23
+ self
24
+ else
25
+ Range.new(self.begin, self.end + 1, true)
26
+ end
11
27
  end
12
28
 
13
29
  ##
14
- #Returns the empty status of the range
30
+ #Returns whether the range is empty
15
31
  #
16
32
  #A range is empty if end < begin or if begin == end and exclude_end? is true.
17
33
  def empty?
18
- (self.end < self.begin) || (exclude_end? && (self.begin == self.end))
34
+ if exclude_end?
35
+ self.end <= self.begin
36
+ else
37
+ self.end < self.begin
38
+ end
19
39
  end
20
-
21
- #This code probably works, but we're not using it anywhere.
22
- # def intersection(other)
23
- # self_n = self.normalized
24
- # other = other.normalized
25
- #
26
- # new_begin, new_end, exclude_end = nil
27
- #
28
- # if self_n.cover?(other.begin)
29
- # new_first = other.begin
30
- # elsif other.cover?(self_n.begin)
31
- # new_first = self_n.begin
32
- # end
33
- #
34
- # return self_n.begin ... self_n.begin unless new_first
35
- #
36
- # if self_n.cover?(other.end)
37
- # unless other.exclude_end? && other.end == self_n.begin
38
- # new_end = other.end
39
- # exclude_end = other.exclude_end?
40
- # end
41
- # elsif other.cover?(self_n.end)
42
- # unless self_n.exclude_end? && self_n.end == other.begin
43
- # new_end = self_n.end
44
- # exclude_end = self_n.exclude_end?
45
- # end
46
- # end
47
- #
48
- # #If we still haven't found new_end, try one more case:
49
- # unless new_end
50
- # if self_n.end == other.end
51
- # #We'll only get here if both ranges exclude their ends and have the same end.
52
- # new_end = self_n.end
53
- # exclude_end = true
54
- # end
55
- # end
56
- #
57
- # return self_n.begin ... self_n.begin unless new_end
58
- #
59
- # Range.new(new_begin, new_end, exclude_end)
60
- # end
61
40
  end
62
41
 
63
42
  ##
@@ -38,6 +38,7 @@ module Bookie
38
38
 
39
39
  ##
40
40
  #An array containing the labels for each field in a details table
41
+ #TODO: remove some fields?
41
42
  DETAILS_FIELD_LABELS = [
42
43
  'User', 'Group', 'System', 'System type', 'Start time', 'End time', 'Wall time',
43
44
  'CPU time', 'Memory usage', 'Command', 'Exit code'
@@ -54,6 +55,7 @@ module Bookie
54
55
  #
55
56
  #Returns the summaries for <tt>jobs</tt> and <tt>systems</tt>
56
57
  def print_summary(jobs, summaries, systems, time_range = nil)
58
+ jobs = jobs.includes(:user, :group, :system, :system_type)
57
59
  jobs_summary = summaries.summary(:jobs => jobs, :range => time_range)
58
60
  num_jobs = jobs_summary[:num_jobs]
59
61
  systems_summary = systems.summary(time_range)
@@ -62,6 +64,7 @@ module Bookie
62
64
  memory_time = jobs_summary[:memory_time]
63
65
  avail_memory_time = systems_summary[:avail_memory_time]
64
66
  successful = (num_jobs == 0) ? 0.0 : Float(jobs_summary[:successful]) / num_jobs
67
+
65
68
  field_values = [
66
69
  num_jobs,
67
70
  Formatter.format_duration(cpu_time),
@@ -72,13 +75,14 @@ module Bookie
72
75
  if avail_memory_time == 0 then '0.0000%' else '%.4f%%' % (Float(memory_time) / avail_memory_time * 100) end
73
76
  ]
74
77
  do_print_summary(field_values)
75
- return jobs_summary, systems_summary
76
78
  end
77
79
 
78
80
  ##
79
81
  #Prints a table containing all details of <tt>jobs</tt>
80
82
  #
81
- #<tt>jobs</tt> should be an array.
83
+ #<tt>jobs</tt> should be an ActiveRecord model or relation.
84
+ #
85
+ #To consider: allow any Enumerable?
82
86
  def print_jobs(jobs)
83
87
  do_print_jobs(jobs)
84
88
  end
@@ -97,7 +101,7 @@ module Bookie
97
101
  #call-seq:
98
102
  # fields_for_each_job(jobs) { |fields| ... }
99
103
  #
100
- #<tt>jobs</tt> should be an array of Bookie::Database::Job objects.
104
+ #<tt>jobs</tt> should be an ActiveRecord model or relation.
101
105
  #
102
106
  #===Examples
103
107
  # formatter.fields_for_each_job(jobs) do |fields|
@@ -117,6 +121,7 @@ module Bookie
117
121
  job.user.name,
118
122
  job.user.group.name,
119
123
  job.system.name,
124
+ #TODO: remove this field?
120
125
  job.system.system_type.name,
121
126
  job.start_time.getlocal.strftime('%Y-%m-%d %H:%M:%S'),
122
127
  job.end_time.getlocal.strftime('%Y-%m-%d %H:%M:%S'),
@@ -128,7 +133,6 @@ module Bookie
128
133
  ]
129
134
  end
130
135
  end
131
- protected :fields_for_each_job
132
136
 
133
137
  ##
134
138
  #Formats a duration in a human-readable format
data/lib/bookie/sender.rb CHANGED
@@ -14,9 +14,9 @@ module Bookie
14
14
  #<tt>config</tt> should be an instance of Bookie::Config.
15
15
  def initialize(config)
16
16
  @config = config
17
- t = @config.system_type
18
- require "bookie/senders/#{t}"
19
- extend Bookie::Senders.const_get(t.camelize)
17
+ sys_type = config.system_type
18
+ require "bookie/senders/#{sys_type}"
19
+ extend Bookie::Senders.const_get(sys_type.camelize)
20
20
  end
21
21
 
22
22
  ##
@@ -47,7 +47,7 @@ module Bookie
47
47
  #Send the job data:
48
48
  each_job(filename) do |job|
49
49
  next if filtered?(job)
50
- model = job.to_model
50
+ model = job.to_record
51
51
  time_min = (model.start_time < time_min) ? model.start_time : time_min
52
52
  time_max = (model.end_time > time_max) ? model.end_time : time_max
53
53
  #To consider: handle files that don't have jobs sorted by end time?
@@ -95,14 +95,14 @@ module Bookie
95
95
  if system.end_time && job.end_time > system.end_time
96
96
  system = Database::System.find_current(self, job.end_time)
97
97
  end
98
- #To consider: optimize this query?
98
+ #TODO: optimize this operation?
99
99
  #(It should be possible to delete all of the jobs with end times between those of the first and last jobs of the file (exclusive),
100
100
  #but jobs with end times matching those of the first/last jobs in the file might be from an earlier or later file, not this one.
101
101
  #This assumes that the files all have jobs sorted by end time.
102
102
  model = duplicate(job, system)
103
103
  break unless model
104
- time_min = (model.start_time < time_min) ? model.start_time : time_min
105
- time_max = (model.end_time > time_max) ? model.end_time : time_max
104
+ time_min = [model.start_time, time_min].min
105
+ time_max = [model.end_time, time_max].max
106
106
  model.delete
107
107
  end
108
108
 
@@ -136,11 +136,11 @@ module Bookie
136
136
  end
137
137
 
138
138
  #Used internally by #send_data and #undo_send
139
- def clear_summaries(date_min, date_max)
139
+ def clear_summaries(date_min, date_max)
140
140
  #Since joins don't mix with DELETE statements, we have to do this the hard way.
141
- systems = Database::System.by_name(@config.hostname).to_a
142
- systems.map!{ |sys| sys.id }
143
- Database::JobSummary.where('job_summaries.system_id in (?)', systems).where('date >= ? AND date <= ?', date_min, date_max).delete_all
141
+ #To consider: prune systems by time?
142
+ system_ids = Database::System.by_name(@config.hostname).pluck(:id)
143
+ Database::JobSummary.where('job_summaries.system_id in (?)', system_ids).where('date >= ? AND date <= ?', date_min, date_max).delete_all
144
144
  end
145
145
  private :clear_summaries
146
146
  end
@@ -150,7 +150,7 @@ module Bookie
150
150
  module ModelHelpers
151
151
  ##
152
152
  #Converts the object to a Bookie::Database::Job
153
- def to_model()
153
+ def to_record()
154
154
  job = Bookie::Database::Job.new
155
155
  job.command_name = self.command_name
156
156
  job.start_time = self.start_time
@@ -161,8 +161,6 @@ module Bookie
161
161
  job
162
162
  end
163
163
 
164
- ##
165
- #Returns the end time
166
164
  def end_time
167
165
  start_time + wall_time
168
166
  end
@@ -1,4 +1,4 @@
1
1
  module Bookie
2
2
  #The library version
3
- VERSION = "1.2.3"
3
+ VERSION = "2.0.0"
4
4
  end
@@ -2,7 +2,7 @@
2
2
  "Database type" : "sqlite3",
3
3
  "Server" : "localhost",
4
4
  "Port" : 8080,
5
- "Database" : "test.sqlite",
5
+ "Database" : ":memory:",
6
6
  "Username" : "blm768",
7
7
  "Password" : "test",
8
8
  "Excluded users": ["root"],
@@ -10,4 +10,4 @@
10
10
  "Hostname" : "localhost",
11
11
  "Cores" : 8,
12
12
  "Memory" : 8000000
13
- }
13
+ }
data/spec/config_spec.rb CHANGED
@@ -9,7 +9,7 @@ describe Bookie::Config do
9
9
  config.db_type.should eql "sqlite3"
10
10
  config.server.should eql "localhost"
11
11
  config.port.should eql 8080
12
- config.database.should eql "test.sqlite"
12
+ config.database.should eql ":memory:"
13
13
  config.username.should eql "blm768"
14
14
  config.password.should eql "test"
15
15
  config.excluded_users.should eql Set.new(["root"])
@@ -52,4 +52,4 @@ describe Bookie::Config do
52
52
  ActiveRecord::Base.time_zone_aware_attributes.should eql true
53
53
  ActiveRecord::Base.default_timezone.should eql :utc
54
54
  end
55
- end
55
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bookie::Database::Group do
4
+ describe "#find_or_create" do
5
+ it "creates the group if needed" do
6
+ Bookie::Database::Group.expects(:"create!")
7
+ Bookie::Database::Group.find_or_create!('non_root')
8
+ end
9
+
10
+ it "returns the cached group if one exists" do
11
+ group = Bookie::Database::Group.find_by_name('root')
12
+ known_groups = {'root' => group}
13
+ Bookie::Database::Group.find_or_create!('root', known_groups).should equal group
14
+ end
15
+
16
+ it "queries the database when this group is not cached" do
17
+ group = Bookie::Database::Group.find_by_name('root')
18
+ known_groups = {}
19
+ Bookie::Database::Group.expects(:find_by_name).returns(group).twice
20
+ Bookie::Database::Group.expects(:"create!").never
21
+ Bookie::Database::Group.find_or_create!('root', known_groups).should eql group
22
+ Bookie::Database::Group.find_or_create!('root', nil).should eql group
23
+ known_groups.should include 'root'
24
+ end
25
+ end
26
+
27
+ it "validates the name field" do
28
+ group = Bookie::Database::Group.new(:name => nil)
29
+ group.valid?.should eql false
30
+ group.name = ''
31
+ group.valid?.should eql false
32
+ group.name = 'test'
33
+ group.valid?.should eql true
34
+ end
35
+ end
36
+
@@ -0,0 +1,308 @@
1
+ require 'spec_helper'
2
+
3
+ include Bookie::Database
4
+
5
+ RSpec::Matchers.define :be_job_within_time_range do |time_range|
6
+ match do |job|
7
+ expect(time_range).to cover(job.start_time)
8
+ #This check is required because of a peculiarity of #within_time_range;
9
+ #jobs with an end_time one second beyond the last value in the range
10
+ #are still included (intentionally).
11
+ range_extended = Range.new(time_range.begin, time_range.end + 1, time_range.exclude_end?)
12
+ expect(range_extended).to cover(job.end_time)
13
+ end
14
+ end
15
+
16
+ describe Bookie::Database::Job do
17
+ it "correctly sets end times" do
18
+ Job.find_each do |job|
19
+ job.end_time.should eql job.start_time + job.wall_time
20
+ job.end_time.should eql job.read_attribute(:end_time)
21
+ end
22
+
23
+ #Test the update hook.
24
+ job = Job.first
25
+ job.start_time -= 1
26
+ job.save!
27
+ job.end_time.should eql job.read_attribute(:end_time)
28
+ end
29
+
30
+ describe "#end_time=" do
31
+ it "correctly adjusts time values" do
32
+ job = Job.first
33
+ old_end_time = job.end_time
34
+ job.end_time -= 1
35
+ #We use #to_i because directly subtracting Time objects produces
36
+ #floating-point numbers.
37
+ expect(job.wall_time).to eql(job.end_time.to_i - job.start_time.to_i)
38
+ expect(job.end_time).to eql(old_end_time - 1)
39
+ end
40
+ end
41
+
42
+ it "correctly filters by user" do
43
+ user = User.by_name('test').order(:id).first
44
+ jobs = Job.by_user(user).to_a
45
+ jobs.each do |job|
46
+ job.user.should eql user
47
+ end
48
+ jobs.length.should eql 10
49
+ end
50
+
51
+ it "correctly filters by user name" do
52
+ jobs = Job.by_user_name('root').to_a
53
+ jobs.length.should eql 10
54
+ jobs[0].user.name.should eql "root"
55
+ jobs = Job.by_user_name('test').order(:end_time).to_a
56
+ jobs.length.should eql 20
57
+ jobs.each do |job|
58
+ job.user.name.should eql 'test'
59
+ end
60
+ jobs[0].user_id.should_not eql jobs[-1].user_id
61
+ jobs = Job.by_user_name('user').to_a
62
+ jobs.length.should eql 0
63
+ end
64
+
65
+ it "correctly filters by group name" do
66
+ jobs = Job.by_group_name("root").to_a
67
+ jobs.length.should eql 10
68
+ jobs.each do |job|
69
+ job.user.group.name.should eql "root"
70
+ end
71
+ jobs = Job.by_group_name("admin").order(:start_time).to_a
72
+ jobs.length.should eql 20
73
+ jobs[0].user.name.should_not eql jobs[1].user.name
74
+ jobs = Job.by_group_name("test").to_a
75
+ jobs.length.should eql 0
76
+ end
77
+
78
+ it "correctly filters by system" do
79
+ sys = System.first
80
+ jobs = Job.by_system(sys)
81
+ jobs.length.should eql 10
82
+ jobs.each do |job|
83
+ job.system.should eql sys
84
+ end
85
+ end
86
+
87
+ it "correctly filters by system name" do
88
+ jobs = Job.by_system_name('test1')
89
+ jobs.length.should eql 20
90
+ jobs = Job.by_system_name('test2')
91
+ jobs.length.should eql 10
92
+ jobs = Job.by_system_name('test3')
93
+ jobs.length.should eql 10
94
+ jobs = Job.by_system_name('test4')
95
+ jobs.length.should eql 0
96
+ end
97
+
98
+ it "correctly filters by system type" do
99
+ sys_type = SystemType.find_by_name('Standalone')
100
+ jobs = Job.by_system_type(sys_type)
101
+ jobs.length.should eql 20
102
+ sys_type = SystemType.find_by_name('TORQUE cluster')
103
+ jobs = Job.by_system_type(sys_type)
104
+ jobs.length.should eql 20
105
+ end
106
+
107
+ it "correctly filters by command name" do
108
+ jobs = Job.by_command_name('vi')
109
+ jobs.length.should eql 20
110
+ jobs = Job.by_command_name('emacs')
111
+ jobs.length.should eql 20
112
+ end
113
+
114
+ describe "#by_time_range" do
115
+ let(:base_start) { base_time + 1.hours }
116
+ let(:base_end) { base_start + 2.hours }
117
+
118
+ it "filters by inclusive time range" do
119
+ jobs = Job.by_time_range(base_start ... base_end + 1)
120
+ jobs.count.should eql 3
121
+ jobs = Job.by_time_range(base_start + 1 ... base_end)
122
+ jobs.count.should eql 2
123
+ jobs = Job.by_time_range(base_start ... base_start)
124
+ jobs.length.should eql 0
125
+ end
126
+
127
+ it "filters by exclusive time range" do
128
+ jobs = Job.by_time_range(base_start + 1 .. base_end)
129
+ jobs.count.should eql 3
130
+ jobs = Job.by_time_range(base_start .. base_end - 1)
131
+ jobs.count.should eql 2
132
+ jobs = Job.by_time_range(base_start .. base_start)
133
+ jobs.count.should eql 1
134
+ end
135
+
136
+ context "with an empty range" do
137
+ it "finds no jobs" do
138
+ (-1 .. 0).each do |offset|
139
+ jobs = Job.by_time_range(base_start ... base_start + offset)
140
+ jobs.count.should eql 0
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ describe "#within_time_range" do
147
+ let(:base_start) { base_time + 1.hours + 1 }
148
+ let(:base_end) { base_time + 5.hours - 1 }
149
+
150
+ it "finds jobs within the range" do
151
+ [true, false].each do |exclude_end|
152
+ time_range = Range.new(base_start, base_end, exclude_end)
153
+ jobs = Job.within_time_range(time_range)
154
+ expect(jobs.count).to eql (exclude_end ? 2 : 3)
155
+ jobs.each do |job|
156
+ expect(job).to be_job_within_time_range(time_range)
157
+ end
158
+ end
159
+ end
160
+
161
+ context "with an empty range" do
162
+ it "finds no jobs" do
163
+ jobs = Job.within_time_range(base_start ... base_start)
164
+ expect(jobs.count).to eql(0)
165
+ end
166
+ end
167
+ end
168
+
169
+ describe "overlapping_edges" do
170
+ let(:base_start) { base_time }
171
+ let(:base_end) { base_time + 3600 }
172
+
173
+ context "with exclusive ranges" do
174
+ it "finds jobs that overlap the edges" do
175
+ #Overlapping beginning
176
+ jobs = Job.overlapping_edges(base_start + 1 ... base_end)
177
+ expect(jobs.count).to eql 1
178
+ expect(jobs.first.start_time).to eql base_time
179
+
180
+ #Overlapping end
181
+ jobs = Job.overlapping_edges(base_start ... base_end - 1)
182
+ expect(jobs.count).to eql 1
183
+ expect(jobs.first.start_time).to eql base_time
184
+
185
+ #One job overlapping both
186
+ jobs = Job.overlapping_edges(base_start + 1 ... base_end - 1)
187
+ expect(jobs.count).to eql 1
188
+ expect(jobs.first.start_time).to eql base_time
189
+
190
+ #Two jobs overlapping the endpoints
191
+ jobs = Job.overlapping_edges(base_end - 1 ... base_end + 1)
192
+ expect(jobs.count).to eql 2
193
+
194
+ #Not overlapping any endpoints
195
+ jobs = Job.overlapping_edges(base_start ... base_end)
196
+ expect(jobs.count).to eql 0
197
+ end
198
+ end
199
+
200
+ context "with inclusive ranges" do
201
+ it "finds jobs that overlap the edges" do
202
+ #This is more pared-down because inclusive and exclusive ranges mostly
203
+ #share the same codepath.
204
+ jobs = Job.overlapping_edges(base_start .. base_end)
205
+ expect(jobs.count).to eql 1
206
+
207
+ jobs = Job.overlapping_edges(base_start .. base_start)
208
+ expect(jobs.count).to eql 1
209
+ end
210
+ end
211
+
212
+ context "with empty ranges" do
213
+ it "finds no jobs" do
214
+ jobs = Job.overlapping_edges(base_start + 1 ... base_start + 1)
215
+ expect(jobs.count).to eql 0
216
+ jobs = Job.overlapping_edges(base_start + 1 .. base_start)
217
+ expect(jobs.count).to eql 0
218
+ end
219
+ end
220
+ end
221
+
222
+ describe "#summary" do
223
+ let(:count) { Job.count }
224
+ let(:summary) { create_summaries(Job, base_time) }
225
+
226
+ #TODO: test the case where a job extends on both sides of the summary range?
227
+ it "produces correct summary totals" do
228
+ expect(summary[:all]).to eql({
229
+ :num_jobs => count,
230
+ :successful => 20,
231
+ :cpu_time => count * 100,
232
+ :memory_time => count * 200 * 1.hour,
233
+ })
234
+
235
+ expect(summary[:all_constrained]).to eql(summary[:all])
236
+ expect(summary[:wide]).to eql(summary[:all])
237
+
238
+ expect(summary[:all_filtered]).to eql({
239
+ :num_jobs => count / 2,
240
+ :successful => 20,
241
+ :cpu_time => count * 100 / 2,
242
+ :memory_time => count * 100 * 1.hour,
243
+ })
244
+
245
+ num_clipped_jobs = summary[:clipped][:num_jobs]
246
+ expect(summary[:clipped]).to eql({
247
+ :num_jobs => 25,
248
+ :cpu_time => num_clipped_jobs * 100 - 50,
249
+ #TODO: this seems off. Why?
250
+ :memory_time => num_clipped_jobs * 200 * 3600 - 100 * 3600,
251
+ :successful => num_clipped_jobs / 2 + 1,
252
+ })
253
+ end
254
+
255
+ it "correctly handles summaries of empty sets" do
256
+ summary[:empty].should eql({
257
+ :num_jobs => 0,
258
+ :cpu_time => 0,
259
+ :memory_time => 0,
260
+ :successful => 0,
261
+ })
262
+ end
263
+
264
+ it "correctly handles inverted ranges" do
265
+ Job.summary(Time.now() ... Time.now() - 1).should eql summary[:empty]
266
+ Job.summary(Time.now() .. Time.now() - 1).should eql summary[:empty]
267
+ end
268
+
269
+ it "distinguishes between inclusive and exclusive ranges" do
270
+ sum = Job.summary(base_time ... base_time + 3600)
271
+ sum[:num_jobs].should eql 1
272
+ sum = Job.summary(base_time .. base_time + 3600)
273
+ sum[:num_jobs].should eql 2
274
+ end
275
+ end
276
+
277
+ it "validates fields" do
278
+ fields = {
279
+ :user => User.first,
280
+ :system => System.first,
281
+ :command_name => '',
282
+ :cpu_time => 100,
283
+ :start_time => base_time,
284
+ :wall_time => 1000,
285
+ :memory => 10000,
286
+ :exit_code => 0
287
+ }
288
+
289
+ job = Job.new(fields)
290
+ job.valid?.should eql true
291
+
292
+ fields.each_key do |field|
293
+ job = Job.new(fields)
294
+ job.method("#{field}=".intern).call(nil)
295
+ job.valid?.should eql false
296
+ end
297
+
298
+ [:cpu_time, :wall_time, :memory].each do |field|
299
+ job = Job.new(fields)
300
+ m = job.method("#{field}=".intern)
301
+ m.call(-1)
302
+ job.valid?.should eql false
303
+ m.call(0)
304
+ job.valid?.should eql true
305
+ end
306
+ end
307
+ end
308
+