pacct 0.8.0-universal-linux

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pacct.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Pacct
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pacct'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install pacct
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ task.rspec_opts =%w{--color --format progress}
9
+ task.pattern = 'spec/*_spec.rb'
10
+ end
11
+
12
+ task :docs do
13
+ system("rdoc --exclude '(./)?spec/'")
14
+ end
15
+
@@ -0,0 +1,9 @@
1
+ require 'mkmf'
2
+
3
+ =begin
4
+ if ENV['COVERAGE']
5
+ $CFLAGS << ' -fprofile-arcs -ftest-coverage'
6
+ end
7
+ =end
8
+
9
+ create_makefile("pacct/pacct_c");
@@ -0,0 +1,856 @@
1
+ #include <errno.h>
2
+ #include <stdio.h>
3
+ #include <stdlib.h>
4
+
5
+ #include <grp.h>
6
+ #include <pwd.h>
7
+ #include <unistd.h>
8
+ #include <sys/acct.h>
9
+ #include <sys/types.h>
10
+
11
+ #include "ruby.h"
12
+
13
+ static char const* validFileModes[] = {
14
+ "rb",
15
+ "wb",
16
+ "r+b",
17
+ "w+b",
18
+ };
19
+
20
+ static VALUE mPacct;
21
+ static VALUE cLog;
22
+ static VALUE cEntry;
23
+
24
+ //Classes from Ruby
25
+ static VALUE cTime;
26
+ static VALUE cNoMemoryError;
27
+ static VALUE cSystemCallError;
28
+
29
+ //Identifiers
30
+ static ID id_at;
31
+ static ID id_new;
32
+ static ID id_to_i;
33
+
34
+ //To do: where is a better place to put these?
35
+ static VALUE known_users_by_name = Qnil;
36
+ static VALUE known_groups_by_name = Qnil;
37
+ static VALUE known_users_by_id = Qnil;
38
+ static VALUE known_groups_by_id = Qnil;
39
+
40
+ //System parameters
41
+ static int pageSize;
42
+ static long ticksPerSecond;
43
+
44
+ //Converts a comp_t to a long
45
+ static unsigned long comp_t_to_ulong(comp_t c) {
46
+ return (unsigned long)(c & 0x1fff) << (((c >> 13) & 0x7) * 3);
47
+ }
48
+
49
+ //Prints a number in binary (for debugging)
50
+ static void print_bin(unsigned long val) {
51
+ //Cast prevents warning
52
+ unsigned long bits = (unsigned long)1 << (sizeof(unsigned long) * 8 - 1);
53
+ putchar('0' + ((val & bits) > 0));
54
+ while(bits >>= 1) {
55
+ putchar('0' + ((val & bits) > 0));
56
+ }
57
+ putchar('\n');
58
+ }
59
+
60
+ //Converts a long to a comp_t
61
+ //To consider: make sure the value is positive?
62
+ //To consider: more unit testing?
63
+ static comp_t ulong_to_comp_t(unsigned long l) {
64
+ size_t bits = 0;
65
+ unsigned long l2 = l;
66
+ if(l2) {
67
+ bits = 1;
68
+ while(l2 >>= 1) {
69
+ ++bits;
70
+ }
71
+ }
72
+ if(bits <= 13) {
73
+ return (l & 0x1fff);
74
+ } else {
75
+ size_t div_bits, rem_bits;
76
+ bits -= 13;
77
+ div_bits = bits / 3;
78
+ if(div_bits >= 8) {
79
+ rb_raise(rb_eRangeError, "Exponent overflow in ulong_to_comp_t: Value %lu is too large.", l);
80
+ }
81
+ rem_bits = bits - div_bits * 3;
82
+ if(rem_bits) {
83
+ div_bits += 1;
84
+ }
85
+ //To consider: remove '&'?
86
+ return ((l >> bits) & 0x1fff) | ((div_bits & 0x7) << 13);
87
+ }
88
+ }
89
+
90
+ //Checks the result of a call, raising an error if it fails
91
+ #define CHECK_CALL(expr, expected_result) \
92
+ { \
93
+ typeof(expr) expected = (expected_result); \
94
+ typeof(expr) result; \
95
+ errno = 0; \
96
+ result = (expr); \
97
+ if(result != expected) { \
98
+ if(errno) { \
99
+ char buf[512]; \
100
+ VALUE err; \
101
+ snprintf(buf, sizeof(buf), "%s(%u)", __FILE__, __LINE__); \
102
+ err = rb_funcall(cSystemCallError, id_new, 2, rb_str_new2(buf), INT2NUM(errno)); \
103
+ rb_exc_raise(err); \
104
+ } else { \
105
+ char buf[512]; \
106
+ snprintf(buf, sizeof(buf), #expr ": result %i expected, not %i - %s(%u)", expected, result, __FILE__, __LINE__); \
107
+ rb_raise(rb_eRuntimeError, buf); \
108
+ } \
109
+ } \
110
+ } \
111
+
112
+ #define ENSURE_ALLOCATED(ptr) if(!ptr) rb_raise(cNoMemoryError, "Out of memory");
113
+
114
+ typedef struct {
115
+ FILE* file;
116
+ char* filename;
117
+ long num_entries;
118
+ } PacctLog;
119
+
120
+ static void pacct_log_free(void* p) {
121
+ PacctLog* log = (PacctLog*) p;
122
+ if(log->file) {
123
+ fclose(log->file);
124
+ log->file = NULL;
125
+ }
126
+ free(log->filename);
127
+ free(p);
128
+ }
129
+
130
+ /*
131
+ *call-seq:
132
+ * new(filename)
133
+ *
134
+ *Creates a new Pacct::Log using the given accounting file
135
+ */
136
+ static VALUE pacct_log_new(int argc, VALUE* argv, VALUE class) {
137
+ VALUE log;
138
+ VALUE init_args[2];
139
+ PacctLog* ptr;
140
+
141
+ init_args[1] = Qnil;
142
+ rb_scan_args(argc, argv, "11", init_args, init_args + 1);
143
+
144
+ log = Data_Make_Struct(class, PacctLog, 0, pacct_log_free, ptr);
145
+
146
+ ptr->file = NULL;
147
+ ptr->num_entries = 0;
148
+
149
+ rb_obj_call_init(log, 2, init_args);
150
+ return log;
151
+ }
152
+
153
+ static VALUE pacct_log_init(VALUE self, VALUE filename, VALUE mode) {
154
+ PacctLog* log;
155
+ FILE* acct;
156
+ long length;
157
+ char* c_filename = StringValueCStr(filename);
158
+ size_t c_filename_len;
159
+ const char* c_mode = "rb";
160
+
161
+ if(mode != Qnil) {
162
+ int isValidMode = 0;
163
+ size_t i;
164
+ c_mode = StringValueCStr(mode);
165
+ for(i = 0; i < sizeof(validFileModes) / sizeof(char*); ++i) {
166
+ if(strcmp(c_mode, validFileModes[i]) == 0) {
167
+ isValidMode = 1;
168
+ break;
169
+ }
170
+ }
171
+ if(!isValidMode) {
172
+ rb_raise(rb_eArgError, "Invalid mode for Pacct::File: '%s'", c_mode);
173
+ }
174
+ }
175
+
176
+ acct = fopen(c_filename, c_mode);
177
+ if(!acct) {
178
+ rb_raise(rb_eIOError, "Unable to open file '%s'", c_filename);
179
+ }
180
+
181
+ Data_Get_Struct(self, PacctLog, log);
182
+
183
+ log->file = acct;
184
+ c_filename_len = strlen(c_filename);
185
+ log->filename = malloc(c_filename_len + 1);
186
+ ENSURE_ALLOCATED(log->filename);
187
+ strncpy(log->filename, c_filename, c_filename_len);
188
+ log->filename[c_filename_len] = '\0';
189
+
190
+ CHECK_CALL(fseek(acct, 0, SEEK_END), 0);
191
+ length = ftell(acct);
192
+ rewind(acct);
193
+
194
+ if(length % sizeof(struct acct_v3) != 0) {
195
+ rb_raise(rb_eIOError, "Accounting file '%s' appears to be the wrong size.", c_filename);
196
+ }
197
+
198
+ log->num_entries = length / sizeof(struct acct_v3);
199
+
200
+ return self;
201
+ }
202
+
203
+ static void pacct_log_check_closed(PacctLog* log) {
204
+ if(!log->file) {
205
+ rb_raise(rb_eRuntimeError, "The file '%s' has already been closed.", log->filename);
206
+ }
207
+ }
208
+
209
+ /*
210
+ *Closes the log file
211
+ */
212
+ static VALUE pacct_log_close(VALUE self) {
213
+ PacctLog* log;
214
+
215
+ Data_Get_Struct(self, PacctLog, log);
216
+
217
+ if(log->file) {
218
+ fclose(log->file);
219
+ log->file = NULL;
220
+ }
221
+
222
+ return Qnil;
223
+ }
224
+
225
+ static VALUE pacct_entry_new(PacctLog* log) {
226
+ struct acct_v3* ptr;
227
+ VALUE entry = Data_Make_Struct(cEntry, struct acct_v3, 0, free, ptr);
228
+ if(log) {
229
+ size_t entries_read;
230
+ pacct_log_check_closed(log);
231
+ entries_read = fread(ptr, sizeof(struct acct_v3), 1, log->file);
232
+ if(entries_read != 1) {
233
+ rb_raise(rb_eIOError, "Unable to read record from accounting file '%s'", log->filename);
234
+ }
235
+ } else {
236
+ memset(ptr, 0, sizeof(struct acct_v3));
237
+ }
238
+
239
+ return entry;
240
+ }
241
+
242
+ //This is the version of pacct_entry_new that is actually exposed to Ruby.
243
+ static VALUE ruby_pacct_entry_new(VALUE self) {
244
+ return pacct_entry_new(NULL);
245
+ }
246
+
247
+ /*
248
+ *call-seq:
249
+ * each_entry([start]) {|entry, index| ...}
250
+ *
251
+ *Yields each entry in the file to the given block
252
+ *
253
+ *If start is given, iteration starts at the entry with that index.
254
+ */
255
+ static VALUE each_entry(int argc, VALUE* argv, VALUE self) {
256
+ PacctLog* log;
257
+ VALUE start_value;
258
+ long start = 0;
259
+ int i = 0;
260
+
261
+ rb_scan_args(argc, argv, "01", &start_value);
262
+ if(argc && start_value != Qnil) {
263
+ start = NUM2UINT(start_value);
264
+ }
265
+
266
+ Data_Get_Struct(self, PacctLog, log);
267
+
268
+ pacct_log_check_closed(log);
269
+
270
+ if(start > log->num_entries) {
271
+ rb_raise(rb_eRangeError, "Index %li is out of range", start);
272
+ }
273
+
274
+ CHECK_CALL(fseek(log->file, start * sizeof(struct acct_v3), SEEK_SET), 0);
275
+
276
+ for(i = start; i < log->num_entries; ++i) {
277
+ VALUE entry = pacct_entry_new(log);
278
+ rb_yield(entry);
279
+ }
280
+
281
+ return Qnil;
282
+ }
283
+
284
+ /*
285
+ *Returns the last entry in the file
286
+ */
287
+ static VALUE last_entry(VALUE self) {
288
+ PacctLog* log;
289
+ long pos;
290
+ VALUE entry;
291
+
292
+ Data_Get_Struct(self, PacctLog, log);
293
+
294
+ pacct_log_check_closed(log);
295
+
296
+ if(log->num_entries == 0) {
297
+ return Qnil;
298
+ }
299
+
300
+ pos = ftell(log->file);
301
+ CHECK_CALL(fseek(log->file, -sizeof(struct acct_v3), SEEK_END), 0);
302
+
303
+ entry = pacct_entry_new(log);
304
+
305
+ CHECK_CALL(fseek(log->file, pos, SEEK_SET), 0);
306
+
307
+ return entry;
308
+ }
309
+
310
+ /*
311
+ *Returns the number of entries in the file
312
+ */
313
+ static VALUE get_num_entries(VALUE self) {
314
+ PacctLog* log;
315
+
316
+ Data_Get_Struct(self, PacctLog, log);
317
+
318
+ return INT2NUM(log->num_entries);
319
+ }
320
+
321
+ /*
322
+ *call-seq:
323
+ * write_entry(entry)
324
+ *
325
+ * Appends the given entry to the file
326
+ */
327
+ static VALUE write_entry(VALUE self, VALUE entry) {
328
+ //To do consider: verification?
329
+ PacctLog* log;
330
+ long pos;
331
+ struct acct_v3* acct;
332
+
333
+ Data_Get_Struct(self, PacctLog, log);
334
+ pacct_log_check_closed(log);
335
+ Data_Get_Struct(entry, struct acct_v3, acct);
336
+
337
+ pos = ftell(log->file);
338
+ CHECK_CALL(fseek(log->file, 0, SEEK_END), 0);
339
+
340
+ if(fwrite(acct, sizeof(struct acct_v3), 1, log->file) != 1) {
341
+ rb_raise(rb_eIOError, "Unable to write to accounting file '%s'", log->filename);
342
+ }
343
+
344
+ ++(log->num_entries);
345
+
346
+ CHECK_CALL(fseek(log->file, pos, SEEK_SET), 0);
347
+
348
+ return Qnil;
349
+ }
350
+
351
+ //Methods of Pacct::Entry
352
+ /*
353
+ *Returns the process ID
354
+ */
355
+ static VALUE get_process_id(VALUE self) {
356
+ struct acct_v3* data;
357
+ Data_Get_Struct(self, struct acct_v3, data);
358
+
359
+ return INT2NUM(data->ac_pid);
360
+ }
361
+
362
+ /*
363
+ *Sets the process ID
364
+ */
365
+ static VALUE set_process_id(VALUE self, VALUE pid) {
366
+ struct acct_v3* data;
367
+ Data_Get_Struct(self, struct acct_v3, data);
368
+
369
+ data->ac_pid = NUM2UINT(pid);
370
+
371
+ return Qnil;
372
+ }
373
+
374
+ /*
375
+ *Returns the ID of the user who executed the command
376
+ */
377
+ static VALUE get_user_id(VALUE self) {
378
+ struct acct_v3* data;
379
+ Data_Get_Struct(self, struct acct_v3, data);
380
+
381
+ return INT2NUM(data->ac_uid);
382
+ }
383
+
384
+ /*
385
+ *Returns the name of the user who executed the command
386
+ */
387
+ static VALUE get_user_name(VALUE self) {
388
+ struct acct_v3* data;
389
+ struct passwd* pw_data;
390
+ VALUE id, name;
391
+ Data_Get_Struct(self, struct acct_v3, data);
392
+
393
+ //If there's a cached user name, return it.
394
+ id = UINT2NUM(data->ac_uid);
395
+ name = rb_hash_aref(known_users_by_id, id);
396
+ if(name != Qnil) {
397
+ return name;
398
+ }
399
+
400
+ //Otherwise, get the user name from the OS.
401
+ errno = 0;
402
+ pw_data = getpwuid(data->ac_uid);
403
+ if(!pw_data) {
404
+ char buf[512];
405
+ VALUE err;
406
+ int e = errno;
407
+ snprintf(buf, 512, "Unable to obtain user name for ID %u", data->ac_uid);
408
+ if(e == 0) {
409
+ e = ENODATA;
410
+ }
411
+ err = rb_funcall(cSystemCallError, id_new, 2, rb_str_new2(buf), INT2NUM(e));
412
+ rb_exc_raise(err);
413
+ }
414
+
415
+ //Cache the user name.
416
+ name = rb_str_new2(pw_data->pw_name);
417
+ rb_hash_aset(known_users_by_id, id, name);
418
+
419
+ return name;
420
+ }
421
+
422
+ /*
423
+ *Sets the name of the user who executed the command
424
+ */
425
+ static VALUE set_user_name(VALUE self, VALUE name) {
426
+ struct acct_v3* data;
427
+ struct passwd* pw_data;
428
+ char* c_name = StringValueCStr(name);
429
+ VALUE id;
430
+ Data_Get_Struct(self, struct acct_v3, data);
431
+
432
+ id = rb_hash_aref(known_users_by_name, name);
433
+ if(id != Qnil) {
434
+ data->ac_uid = NUM2UINT(id);
435
+ return Qnil;
436
+ }
437
+
438
+ errno = 0;
439
+ pw_data = getpwnam(c_name);
440
+ if(!pw_data) {
441
+ char buf[512];
442
+ VALUE err;
443
+ int e = errno;
444
+ snprintf(buf, 512, "Unable to obtain user ID for name '%s'", c_name);
445
+ if(e == 0) {
446
+ e = ENODATA;
447
+ }
448
+ err = rb_funcall(cSystemCallError, id_new, 2, rb_str_new2(buf), INT2NUM(e));
449
+ rb_exc_raise(err);
450
+ }
451
+
452
+ id = UINT2NUM(pw_data->pw_uid);
453
+ rb_hash_aset(known_users_by_name, name, id);
454
+
455
+ data->ac_uid = pw_data->pw_uid;
456
+
457
+ return Qnil;
458
+ }
459
+
460
+ /*
461
+ *Returns the group ID of the user who executed the command
462
+ */
463
+ static VALUE get_group_id(VALUE self) {
464
+ struct acct_v3* data;
465
+ Data_Get_Struct(self, struct acct_v3, data);
466
+
467
+ return INT2NUM(data->ac_gid);
468
+ }
469
+
470
+ /*
471
+ *Returns the group name of the user who executed the command
472
+ */
473
+ static VALUE get_group_name(VALUE self) {
474
+ struct acct_v3* data;
475
+ struct group* group_data;
476
+ VALUE name, id;
477
+ Data_Get_Struct(self, struct acct_v3, data);
478
+
479
+ id = UINT2NUM(data->ac_gid);
480
+ name = rb_hash_aref(known_groups_by_id, id);
481
+ if(name != Qnil) {
482
+ return name;
483
+ }
484
+
485
+ errno = 0;
486
+ group_data = getgrgid(data->ac_gid);
487
+ if(!group_data) {
488
+ char buf[512];
489
+ VALUE err;
490
+ int e = errno;
491
+ snprintf(buf, 512, "Unable to obtain group name for ID %u", data->ac_gid);
492
+ if(e == 0) {
493
+ e = ENODATA;
494
+ }
495
+ err = rb_funcall(cSystemCallError, id_new, 2, rb_str_new2(buf), INT2NUM(e));
496
+ rb_exc_raise(err);
497
+ }
498
+
499
+ name = rb_str_new2(group_data->gr_name);
500
+ rb_hash_aset(known_groups_by_id, id, name);
501
+
502
+ return name;
503
+ }
504
+
505
+ /*
506
+ *Sets the group name of the user who executed the command
507
+ */
508
+ static VALUE set_group_name(VALUE self, VALUE name) {
509
+ struct acct_v3* data;
510
+ struct group* group_data;
511
+ VALUE id;
512
+ char* c_name = StringValueCStr(name);
513
+ Data_Get_Struct(self, struct acct_v3, data);
514
+
515
+ id = rb_hash_aref(known_groups_by_name, name);
516
+ if(id != Qnil) {
517
+ data->ac_gid = NUM2UINT(id);
518
+ return Qnil;
519
+ }
520
+
521
+ errno = 0;
522
+ group_data = getgrnam(c_name);
523
+ if(!group_data) {
524
+ char buf[512];
525
+ VALUE err;
526
+ int e = errno;
527
+ snprintf(buf, 512, "Unable to obtain group ID for name '%s'", c_name);
528
+ if(e == 0) {
529
+ e = ENODATA;
530
+ }
531
+ err = rb_funcall(cSystemCallError, id_new, 2, rb_str_new2(buf), INT2NUM(e));
532
+ rb_exc_raise(err);
533
+ }
534
+
535
+ id = UINT2NUM(group_data->gr_gid);
536
+ rb_hash_aset(known_groups_by_name, name, id);
537
+
538
+ data->ac_gid = group_data->gr_gid;
539
+
540
+ return Qnil;
541
+ }
542
+
543
+ /*
544
+ *Returns the task's total user CPU time in seconds
545
+ */
546
+ static VALUE get_user_time(VALUE self) {
547
+ struct acct_v3* data;
548
+ Data_Get_Struct(self, struct acct_v3, data);
549
+
550
+ return INT2NUM(comp_t_to_ulong(data->ac_utime) / ticksPerSecond);
551
+ }
552
+
553
+ /*
554
+ *Sets the task's total user CPU time
555
+ */
556
+ static VALUE set_user_time(VALUE self, VALUE value) {
557
+ struct acct_v3* data;
558
+ Data_Get_Struct(self, struct acct_v3, data);
559
+
560
+ data->ac_utime = ulong_to_comp_t(NUM2ULONG(value) * ticksPerSecond);
561
+
562
+ return Qnil;
563
+ }
564
+
565
+ /*
566
+ *Returns the task's total system CPU time in seconds
567
+ */
568
+ static VALUE get_system_time(VALUE self) {
569
+ struct acct_v3* data;
570
+ Data_Get_Struct(self, struct acct_v3, data);
571
+
572
+ return INT2NUM(comp_t_to_ulong(data->ac_stime) / ticksPerSecond);
573
+ }
574
+
575
+ /*
576
+ *Sets the task's total system CPU time
577
+ */
578
+ static VALUE set_system_time(VALUE self, VALUE value) {
579
+ struct acct_v3* data;
580
+ Data_Get_Struct(self, struct acct_v3, data);
581
+
582
+ data->ac_stime = ulong_to_comp_t(NUM2ULONG(value) * ticksPerSecond);
583
+
584
+ return Qnil;
585
+ }
586
+
587
+ /*
588
+ *Returns the task's total CPU time in seconds
589
+ */
590
+ static VALUE get_cpu_time(VALUE self) {
591
+ struct acct_v3* data;
592
+ Data_Get_Struct(self, struct acct_v3, data);
593
+
594
+ return INT2NUM((comp_t_to_ulong(data->ac_utime) + comp_t_to_ulong(data->ac_stime)) / ticksPerSecond);
595
+ }
596
+
597
+ /*
598
+ *Returns the task's total wall time in seconds
599
+ */
600
+ static VALUE get_wall_time(VALUE self) {
601
+ struct acct_v3* data;
602
+ Data_Get_Struct(self, struct acct_v3, data);
603
+
604
+ return rb_float_new(data->ac_etime);
605
+ }
606
+
607
+ /*
608
+ *Sets the task's total wall time
609
+ */
610
+ static VALUE set_wall_time(VALUE self, VALUE value) {
611
+ struct acct_v3* data;
612
+ Data_Get_Struct(self, struct acct_v3, data);
613
+
614
+ data->ac_etime = NUM2DBL(value);
615
+
616
+ return Qnil;
617
+ }
618
+
619
+ /*
620
+ *Returns the task's start time
621
+ */
622
+ static VALUE get_start_time(VALUE self) {
623
+ struct acct_v3* data;
624
+ Data_Get_Struct(self, struct acct_v3, data);
625
+
626
+ return rb_funcall(cTime, id_at, 1, INT2NUM(data->ac_btime));
627
+ }
628
+
629
+ /*
630
+ *Sets the task's start time
631
+ */
632
+ static VALUE set_start_time(VALUE self, VALUE value) {
633
+ struct acct_v3* data;
634
+ Data_Get_Struct(self, struct acct_v3, data);
635
+
636
+ data->ac_btime = NUM2UINT(rb_funcall(value, id_to_i, 0));
637
+
638
+ return Qnil;
639
+ }
640
+
641
+ /*
642
+ *Returns the task's average memory usage in kilobytes
643
+ */
644
+ static VALUE get_average_mem_usage(VALUE self) {
645
+ struct acct_v3* data;
646
+ Data_Get_Struct(self, struct acct_v3, data);
647
+
648
+ //Why divided by page size?
649
+ return INT2NUM(comp_t_to_ulong(data->ac_mem) * 1024 / pageSize);
650
+ }
651
+
652
+ /*
653
+ *Sets the task's average memory usage in kilobytes
654
+ */
655
+ static VALUE set_average_mem_usage(VALUE self, VALUE value) {
656
+ struct acct_v3* data;
657
+ Data_Get_Struct(self, struct acct_v3, data);
658
+
659
+ data->ac_mem = ulong_to_comp_t(NUM2ULONG(value) * pageSize / 1024);
660
+
661
+ return Qnil;
662
+ }
663
+
664
+ /*
665
+ *Returns the first 15 characters of the command name
666
+ */
667
+ static VALUE get_command_name(VALUE self) {
668
+ struct acct_v3* data;
669
+ Data_Get_Struct(self, struct acct_v3, data);
670
+
671
+ return rb_str_new2(data->ac_comm);
672
+ }
673
+
674
+ /*
675
+ *Sets the first 15 characters of the command name
676
+ */
677
+ static VALUE set_command_name(VALUE self, VALUE name) {
678
+ struct acct_v3* data;
679
+ Data_Get_Struct(self, struct acct_v3, data);
680
+
681
+ strncpy(data->ac_comm, StringValueCStr(name), ACCT_COMM - 1);
682
+ data->ac_comm[ACCT_COMM - 1] = '\0';
683
+
684
+ return Qnil;
685
+ }
686
+
687
+ /*
688
+ *Returns the command's exit code
689
+ */
690
+ static VALUE get_exit_code(VALUE self) {
691
+ struct acct_v3* data;
692
+ Data_Get_Struct(self, struct acct_v3, data);
693
+
694
+ return INT2NUM(data->ac_exitcode);
695
+ }
696
+
697
+ /*
698
+ *Sets the command's exit code
699
+ */
700
+ static VALUE set_exit_code(VALUE self, VALUE value) {
701
+ struct acct_v3* data;
702
+ Data_Get_Struct(self, struct acct_v3, data);
703
+
704
+ data->ac_exitcode = NUM2UINT(value);
705
+
706
+ return Qnil;
707
+ }
708
+
709
+ //Unit testing code
710
+
711
+ static VALUE test_check_call_macro(VALUE self, VALUE test) {
712
+ int i = NUM2INT(test);
713
+ switch(i) {
714
+ case 0:
715
+ CHECK_CALL(0, 0);
716
+ break;
717
+ case 1:
718
+ CHECK_CALL(1, 0);
719
+ break;
720
+ case 2:
721
+ CHECK_CALL(errno = 0, 1);
722
+ case 3:
723
+ CHECK_CALL(errno = ERANGE, 0);
724
+ default:
725
+ rb_raise(rb_eRangeError, "Unknown test code %i", i);
726
+ }
727
+ return Qnil;
728
+ }
729
+
730
+ static VALUE test_read_failure(VALUE self) {
731
+ PacctLog log;
732
+ //VALUE entry = pacct_entry_new(NULL);
733
+ const char* filename = "/dev/null";
734
+ log.num_entries = 0;
735
+ log.filename = malloc(strlen(filename) + 1);
736
+ ENSURE_ALLOCATED(log.filename);
737
+ strcpy(log.filename, filename);
738
+ log.file = fopen(log.filename, "r");
739
+
740
+ pacct_entry_new(&log);
741
+ return Qnil;
742
+ }
743
+
744
+ static VALUE test_write_failure(VALUE self) {
745
+ PacctLog* ptr;
746
+ VALUE log = Data_Make_Struct(cLog, PacctLog, 0, pacct_log_free, ptr);
747
+ VALUE entry = pacct_entry_new(NULL);
748
+ const char* filename = "spec/pacct_spec.rb";
749
+ ptr->num_entries = 0;
750
+ ptr->filename = malloc(strlen(filename) + 1);
751
+ ENSURE_ALLOCATED(ptr->filename);
752
+ strcpy(ptr->filename, filename);
753
+ ptr->file = fopen(ptr->filename, "r");
754
+
755
+ write_entry(log, entry);
756
+ return Qnil;
757
+ }
758
+
759
+ static VALUE test_ulong_to_comp_t(VALUE self, VALUE val) {
760
+ unsigned long l = (unsigned long)NUM2ULONG(val);
761
+ comp_t result = ulong_to_comp_t(l);
762
+ return INT2NUM(result);
763
+ }
764
+
765
+ static VALUE test_comp_t_to_ulong(VALUE self, VALUE val) {
766
+ comp_t c = (comp_t)NUM2UINT(val);
767
+ unsigned long result = comp_t_to_ulong(c);
768
+ return ULONG2NUM(result);
769
+ }
770
+
771
+ void Init_pacct_c() {
772
+ VALUE mRSpec;
773
+
774
+ //Get system parameters
775
+ pageSize = getpagesize();
776
+ ticksPerSecond = sysconf(_SC_CLK_TCK);
777
+
778
+ //Get Ruby objects.
779
+ cTime = rb_const_get(rb_cObject, rb_intern("Time"));
780
+ cSystemCallError = rb_const_get(rb_cObject, rb_intern("SystemCallError"));
781
+ cNoMemoryError = rb_const_get(rb_cObject, rb_intern("NoMemoryError"));
782
+
783
+ id_at = rb_intern("at");
784
+ id_new = rb_intern("new");
785
+ id_to_i = rb_intern("to_i");
786
+
787
+ //To consider: is there any place that these can be unregistered?
788
+ rb_gc_register_address(&known_users_by_name);
789
+ rb_gc_register_address(&known_groups_by_name);
790
+ rb_gc_register_address(&known_users_by_id);
791
+ rb_gc_register_address(&known_groups_by_id);
792
+
793
+ known_users_by_name = rb_hash_new();
794
+ known_groups_by_name = rb_hash_new();
795
+ known_users_by_id = rb_hash_new();
796
+ known_groups_by_id = rb_hash_new();
797
+
798
+ //Define Ruby modules/objects/methods.
799
+ mPacct = rb_define_module("Pacct");
800
+ /*
801
+ *Represents an accounting file in acct(5) format
802
+ */
803
+ cLog = rb_define_class_under(mPacct, "Log", rb_cObject);
804
+ /*
805
+ *Document-class: Pacct::Entry
806
+ *
807
+ *Represents an entry in a Pacct::File
808
+ */
809
+ cEntry = rb_define_class_under(mPacct, "Entry", rb_cObject);
810
+ rb_define_singleton_method(cLog, "new", pacct_log_new, -1);
811
+ rb_define_method(cLog, "initialize", pacct_log_init, 2);
812
+ rb_define_method(cLog, "each_entry", each_entry, -1);
813
+ rb_define_method(cLog, "last_entry", last_entry, 0);
814
+ rb_define_method(cLog, "num_entries", get_num_entries, 0);
815
+ rb_define_method(cLog, "write_entry", write_entry, 1);
816
+ rb_define_method(cLog, "close", pacct_log_close, 0);
817
+
818
+ rb_define_singleton_method(cEntry, "new", ruby_pacct_entry_new, 0);
819
+ rb_define_method(cEntry, "process_id", get_process_id, 0);
820
+ rb_define_method(cEntry, "process_id=", set_process_id, 1);
821
+ rb_define_method(cEntry, "user_id", get_user_id, 0);
822
+ rb_define_method(cEntry, "user_name", get_user_name, 0);
823
+ rb_define_method(cEntry, "user_name=", set_user_name, 1);
824
+ rb_define_method(cEntry, "group_id", get_group_id, 0);
825
+ rb_define_method(cEntry, "group_name", get_group_name, 0);
826
+ rb_define_method(cEntry, "group_name=", set_group_name, 1);
827
+ rb_define_method(cEntry, "user_time", get_user_time, 0);
828
+ rb_define_method(cEntry, "user_time=", set_user_time, 1);
829
+ rb_define_method(cEntry, "system_time", get_system_time, 0);
830
+ rb_define_method(cEntry, "system_time=", set_system_time, 1);
831
+ rb_define_method(cEntry, "cpu_time", get_cpu_time, 0);
832
+ rb_define_method(cEntry, "wall_time", get_wall_time, 0);
833
+ rb_define_method(cEntry, "wall_time=", set_wall_time, 1);
834
+ rb_define_method(cEntry, "start_time", get_start_time, 0);
835
+ rb_define_method(cEntry, "start_time=", set_start_time, 1);
836
+ rb_define_method(cEntry, "memory", get_average_mem_usage, 0);
837
+ rb_define_method(cEntry, "memory=", set_average_mem_usage, 1);
838
+ rb_define_method(cEntry, "exit_code", get_exit_code, 0);
839
+ rb_define_method(cEntry, "exit_code=", set_exit_code, 1);
840
+ rb_define_method(cEntry, "command_name", get_command_name, 0);
841
+ rb_define_method(cEntry, "command_name=", set_command_name, 1);
842
+
843
+ //To consider: support other testing frameworks?
844
+
845
+ mRSpec = rb_const_defined(rb_cObject, rb_intern("RSpec"));
846
+ if(mRSpec == Qtrue) {
847
+ /*
848
+ *Document-module: Pacct::Test
849
+ */
850
+ VALUE mTest = rb_define_module_under(mPacct, "Test");
851
+ rb_define_module_function(mTest, "check_call", test_check_call_macro, 1);
852
+ rb_define_module_function(mTest, "write_failure", test_write_failure, 0);
853
+ rb_define_module_function(mTest, "read_failure", test_read_failure, 0);
854
+ rb_define_module_function(mTest, "comp_t_to_ulong", test_comp_t_to_ulong, 1);
855
+ }
856
+ }
data/lib/pacct.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "pacct/version"
2
+
3
+ require "pacct/pacct_c"
4
+
5
+ ##
6
+ #Contains classes for working with process accounting files in acct(5) format
7
+ module Pacct
8
+
9
+ end
10
+
@@ -0,0 +1,4 @@
1
+ module Pacct
2
+ #The library version
3
+ VERSION = "0.8.0"
4
+ end
data/pacct.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pacct/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pacct"
8
+ gem.version = Pacct::VERSION
9
+ gem.authors = ["Ben Merritt"]
10
+ gem.email = ["blm768@gmail.com"]
11
+ gem.description = %q{A C extension library for parsing accounting files in acct(5) format}
12
+ gem.summary = %q{A C extension library for parsing accounting files in acct(5) format}
13
+ gem.homepage = "https://github.com/blm768/bookie"
14
+ gem.platform = Gem::Platform.new('universal-linux')
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.extensions = ["ext/pacct/extconf.rb"]
19
+ gem.test_files = gem.files.grep(%r{^(spec)/})
20
+ gem.require_paths = ["lib"]
21
+
22
+ gem.add_development_dependency('rspec')
23
+ end
data/snapshot/pacct ADDED
Binary file
Binary file
@@ -0,0 +1 @@
1
+ Wrong length!
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacct::Entry do
4
+ it "correctly converts \"comp_t\"s to integers" do
5
+ comp_t_to_ulong = Pacct::Test.method(:comp_t_to_ulong)
6
+ comp_t_to_ulong.call(1).should eql 1
7
+ comp_t_to_ulong.call((1 << 13) | 1).should eql(1 << 3)
8
+ comp_t_to_ulong.call((3 << 13) | 20).should eql(20 << 9)
9
+ end
10
+
11
+ #To do: unit test this (eventually).
12
+ =begin
13
+ it "correctly converts integers to \"comp_t\"s" do
14
+
15
+ end
16
+ =end
17
+
18
+ it "raises an error when a comp_t overflows" do
19
+ e = Pacct::Entry.new
20
+ expect {
21
+ e.memory = 1000000000000
22
+ }.to raise_error(RangeError, /Exponent overflow in ulong_to_comp_t: Value [\d]+ is too large./)
23
+ end
24
+
25
+ it "truncates command names if they become too long" do
26
+ e = Pacct::Entry.new
27
+ e.command_name = 'some_very_long_command_name'
28
+ e.command_name.should eql 'some_very_long_'
29
+ end
30
+
31
+ it "raises an error when encountering unknown user/group IDs" do
32
+ log = Pacct::Log.new('snapshot/pacct_invalid_ids')
33
+ log.each_entry do |entry|
34
+ #This assumes that these users and groups don't actually exist.
35
+ #If, for some odd reason, they _do_ exist, this test will fail.
36
+ expect { entry.user_name }.to raise_error(
37
+ Errno::ENODATA.new('Unable to obtain user name for ID 4294967295').to_s)
38
+ expect { entry.user_name = '_____ _' }.to raise_error(
39
+ Errno::ENODATA.new("Unable to obtain user ID for name '_____ _'").to_s)
40
+ expect { entry.group_name }.to raise_error(
41
+ Errno::ENODATA.new('Unable to obtain group name for ID 4294967295').to_s)
42
+ expect { entry.group_name = '_____ _' }.to raise_error(
43
+ Errno::ENODATA.new("Unable to obtain group ID for name '_____ _'").to_s)
44
+ end
45
+ end
46
+ end
data/spec/log_spec.rb ADDED
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacct::Log do
4
+ before(:each) do
5
+ @log = Pacct::Log.new('snapshot/pacct')
6
+ end
7
+
8
+ it "correctly loads data" do
9
+ n = 0
10
+ @log.each_entry do |entry|
11
+ entry.process_id.should eql 1742
12
+ entry.user_id.should eql 0
13
+ entry.user_name.should eql "root"
14
+ entry.group_id.should eql 0
15
+ entry.group_name.should eql "root"
16
+ entry.command_name.should eql "accton"
17
+ entry.start_time.should eql Time.at(1349741116)
18
+ entry.wall_time.should eql 2.0
19
+ entry.user_time.should eql 0
20
+ entry.system_time.should eql 0
21
+ entry.cpu_time.should eql 0
22
+ entry.memory.should eql 979
23
+ entry.exit_code.should eql 0
24
+ n += 1
25
+ end
26
+ n.should eql 1
27
+ end
28
+
29
+ it "correctly handles the seek parameter" do
30
+ n = 0
31
+ @log.each_entry(1) do |e|
32
+ n += 1
33
+ end
34
+ n.should eql 0
35
+ expect { @log.each_entry(2) { |e| } }.to raise_error(RangeError)
36
+ end
37
+
38
+ it "can read data more than once" do
39
+ 2.times do
40
+ @log.each_entry do |e|
41
+ e.user_name.should eql 'root'
42
+ end
43
+ end
44
+ end
45
+
46
+ it "raises an error if reading fails" do
47
+ expect { Pacct::Test::read_failure }.to raise_error(IOError, "Unable to read record from accounting file '/dev/null'")
48
+ end
49
+
50
+ it "correctly finds the last entry" do
51
+ Helpers::double_log('snapshot/pacct_write') do |log|
52
+ entry = log.last_entry
53
+ entry.should_not eql nil
54
+ entry.exit_code.should eql 1
55
+ end
56
+ Pacct::Log.new('/dev/null').last_entry.should eql nil
57
+ end
58
+
59
+ it "raises an error if the file is not found" do
60
+ expect { Pacct::Log.new('snapshot/abc') }.to raise_error
61
+ end
62
+
63
+ it "raises an error when the file is the wrong size" do
64
+ expect { Pacct::Log.new('snapshot/pacct_invalid_length') }.to raise_error
65
+ end
66
+
67
+ ENTRY_DATA = {
68
+ :process_id => 3,
69
+ :user_name => 'root',
70
+ :group_name => 'root',
71
+ :command_name => 'ls',
72
+ :start_time => Time.local(2012, 1, 1),
73
+ :wall_time => 10.0,
74
+ :user_time => 1,
75
+ :system_time => 1,
76
+ :memory => 100000,
77
+ :exit_code => 2}
78
+
79
+ it "correctly writes entries at the end of the file" do
80
+ e = Pacct::Entry.new
81
+ ENTRY_DATA.each_pair do |key, value|
82
+ e.method((key.to_s + '=').intern).call(value)
83
+ end
84
+ FileUtils.cp('snapshot/pacct', 'snapshot/pacct_write')
85
+ log = Pacct::Log.new('snapshot/pacct_write', 'r+b')
86
+ log.write_entry(e)
87
+ e = log.last_entry
88
+ e.should_not eql nil
89
+ ENTRY_DATA.each_pair do |key, value|
90
+ e.send(key).should eql value
91
+ end
92
+ FileUtils.rm('snapshot/pacct_write')
93
+ end
94
+
95
+ it "raises an error if writing fails" do
96
+ expect { Pacct::Test::write_failure }.to raise_error(IOError, "Unable to write to accounting file 'spec/pacct_spec.rb'")
97
+ end
98
+
99
+ it "creates files when opened in write mode" do
100
+ FileUtils.rm('snapshot/abc') if File.exists?('snapshot/abc')
101
+ log = Pacct::Log.new('snapshot/abc', 'wb')
102
+ File.exists?('snapshot/abc').should eql true
103
+ FileUtils.rm('snapshot/abc')
104
+ end
105
+
106
+ it "raises an error if an attempt is made to access the file after it has been closed" do
107
+ log = Pacct::Log.new('/dev/null')
108
+ log.close
109
+ str = "The file '/dev/null' has already been closed."
110
+ expect { log.each_entry }.to raise_error(str)
111
+ expect { log.write_entry }.to raise_error(str)
112
+ expect { log.last_entry }.to raise_error(str)
113
+ end
114
+ end
115
+
116
+ module Helpers
117
+ def self.double_log(filename)
118
+ FileUtils.cp('snapshot/pacct', filename)
119
+ log = Pacct::Log.new(filename, 'r+b')
120
+ entry = nil
121
+ log.each_entry do |e|
122
+ entry = e
123
+ break
124
+ end
125
+ entry.exit_code = 1
126
+ log.write_entry(entry)
127
+ yield log
128
+ FileUtils.rm(filename)
129
+ end
130
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pacct do
4
+ describe :CHECK_CALL do
5
+ it "only raises an error if the expected result is not given" do
6
+ Pacct::Test::check_call(0)
7
+ expect { Pacct::Test::check_call(1) }.to raise_error(/1: result 0 expected, not 1 - pacct_c\.c\([\d]+\)/)
8
+ end
9
+
10
+ it "raises an error if errno is zero" do
11
+ expect { Pacct::Test::check_call(2) }.to raise_error(/errno = 0: result 1 expected, not 0 - pacct_c\.c\([\d]+\)/)
12
+ end
13
+
14
+ #To consider: test for negative values in setters?
15
+
16
+ it "raises an error if errno is nonzero" do
17
+ expect { Pacct::Test::check_call(3) }.to raise_error(Errno::ERANGE, /Numerical result out of range - pacct_c\.c\([\d]+\)/)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ if ENV['COVERAGE']
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
5
+
6
+ require 'pacct'
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pacct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ prerelease:
6
+ platform: universal-linux
7
+ authors:
8
+ - Ben Merritt
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: A C extension library for parsing accounting files in acct(5) format
31
+ email:
32
+ - blm768@gmail.com
33
+ executables: []
34
+ extensions:
35
+ - ext/pacct/extconf.rb
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - LICENSE.txt
41
+ - README.md
42
+ - Rakefile
43
+ - ext/pacct/extconf.rb
44
+ - ext/pacct/pacct_c.c
45
+ - lib/pacct.rb
46
+ - lib/pacct/version.rb
47
+ - pacct.gemspec
48
+ - snapshot/pacct
49
+ - snapshot/pacct_invalid_ids
50
+ - snapshot/pacct_invalid_length
51
+ - spec/entry_spec.rb
52
+ - spec/log_spec.rb
53
+ - spec/pacct_spec.rb
54
+ - spec/spec_helper.rb
55
+ homepage: https://github.com/blm768/bookie
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 1.8.24
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A C extension library for parsing accounting files in acct(5) format
79
+ test_files:
80
+ - spec/entry_spec.rb
81
+ - spec/log_spec.rb
82
+ - spec/pacct_spec.rb
83
+ - spec/spec_helper.rb