codersdojo 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/app/personal_codersdojo.rb +645 -0
- data/spec/personal_codersdojo_spec.rb +272 -0
- metadata +62 -0
@@ -0,0 +1,645 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "tempfile"
|
5
|
+
require "rexml/document"
|
6
|
+
|
7
|
+
class Scheduler
|
8
|
+
|
9
|
+
def initialize runner
|
10
|
+
@runner = runner
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
@runner.start
|
15
|
+
while true do
|
16
|
+
sleep 1
|
17
|
+
@runner.execute
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class Runner
|
24
|
+
|
25
|
+
attr_accessor :file, :run_command
|
26
|
+
|
27
|
+
def initialize shell, session_provider
|
28
|
+
@filename_formatter = FilenameFormatter.new
|
29
|
+
@shell = shell
|
30
|
+
@session_provider = session_provider
|
31
|
+
end
|
32
|
+
|
33
|
+
def start
|
34
|
+
init_session
|
35
|
+
execute
|
36
|
+
end
|
37
|
+
|
38
|
+
def init_session
|
39
|
+
@step = 0
|
40
|
+
@session_id = @session_provider.generate_id
|
41
|
+
@shell.mkdir_p(@filename_formatter.session_dir @session_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def execute
|
45
|
+
change_time = @shell.ctime @file
|
46
|
+
if change_time == @previous_change_time then
|
47
|
+
return
|
48
|
+
end
|
49
|
+
result = @shell.execute "#{@run_command} #{@file}"
|
50
|
+
state_dir = @filename_formatter.state_dir @session_id, @step
|
51
|
+
@shell.mkdir state_dir
|
52
|
+
@shell.cp @file, state_dir
|
53
|
+
@shell.write_file @filename_formatter.result_file(state_dir), result
|
54
|
+
@step += 1
|
55
|
+
@previous_change_time = change_time
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
class Shell
|
61
|
+
|
62
|
+
MAX_STDOUT_LENGTH = 100000
|
63
|
+
|
64
|
+
def cp source, destination
|
65
|
+
FileUtils.cp source, destination
|
66
|
+
end
|
67
|
+
|
68
|
+
def mkdir dir
|
69
|
+
FileUtils.mkdir dir
|
70
|
+
end
|
71
|
+
|
72
|
+
def mkdir_p dirs
|
73
|
+
FileUtils.mkdir_p dirs
|
74
|
+
end
|
75
|
+
|
76
|
+
def execute command
|
77
|
+
spec_pipe = IO.popen(command, "r")
|
78
|
+
result = spec_pipe.read MAX_STDOUT_LENGTH
|
79
|
+
spec_pipe.close
|
80
|
+
puts result
|
81
|
+
result
|
82
|
+
end
|
83
|
+
|
84
|
+
def write_file filename, content
|
85
|
+
file = File.new filename, "w"
|
86
|
+
file << content
|
87
|
+
file.close
|
88
|
+
end
|
89
|
+
|
90
|
+
def read_file filename
|
91
|
+
file = File.new filename
|
92
|
+
content = file.read
|
93
|
+
file.close
|
94
|
+
content
|
95
|
+
end
|
96
|
+
|
97
|
+
def ctime filename
|
98
|
+
File.new(filename).ctime
|
99
|
+
end
|
100
|
+
|
101
|
+
def make_os_specific text
|
102
|
+
text.gsub('%sh%', shell_extension).gsub('%:%', path_separator).gsub('%rm%', remove_command_name)
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_command_name
|
106
|
+
windows? ? 'delete' : 'rm'
|
107
|
+
end
|
108
|
+
|
109
|
+
def shell_extension
|
110
|
+
windows? ? 'cmd' : 'sh'
|
111
|
+
end
|
112
|
+
|
113
|
+
def path_separator
|
114
|
+
windows? ? ';' : ':'
|
115
|
+
end
|
116
|
+
|
117
|
+
def windows?
|
118
|
+
RUBY_PLATFORM.downcase.include? "windows"
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class SessionIdGenerator
|
124
|
+
|
125
|
+
def generate_id time=Time.new
|
126
|
+
year = format_to_length time.year, 4
|
127
|
+
month = format_to_length time.month, 2
|
128
|
+
day = format_to_length time.day, 2
|
129
|
+
hour = format_to_length time.hour, 2
|
130
|
+
minute = format_to_length time.min, 2
|
131
|
+
second = format_to_length time.sec, 2
|
132
|
+
"#{year}-#{month}-#{day}_#{hour}-#{minute}-#{second}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def format_to_length value, len
|
136
|
+
value.to_s.rjust len,"0"
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
class StateReader
|
142
|
+
|
143
|
+
attr_accessor :session_id, :next_step
|
144
|
+
|
145
|
+
def initialize shell
|
146
|
+
@filename_formatter = FilenameFormatter.new
|
147
|
+
@shell = shell
|
148
|
+
@next_step = 0
|
149
|
+
end
|
150
|
+
|
151
|
+
def state_count
|
152
|
+
Dir.new(@filename_formatter.session_dir @session_id).count - 2
|
153
|
+
end
|
154
|
+
|
155
|
+
def enough_states?
|
156
|
+
state_count >= 2
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_state_dir
|
160
|
+
@filename_formatter.state_dir(@session_id, @next_step)
|
161
|
+
end
|
162
|
+
|
163
|
+
def has_next_state
|
164
|
+
File.exist?(get_state_dir)
|
165
|
+
end
|
166
|
+
|
167
|
+
def read_next_state
|
168
|
+
state = State.new
|
169
|
+
state_dir = get_state_dir
|
170
|
+
state.time = @shell.ctime state_dir
|
171
|
+
state.code = @shell.read_file @filename_formatter.source_code_file(state_dir)
|
172
|
+
state.result = @shell.read_file @filename_formatter.result_file(state_dir)
|
173
|
+
@next_step += 1
|
174
|
+
state
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
class FilenameFormatter
|
180
|
+
|
181
|
+
CODERSDOJO_WORKSPACE = ".codersdojo"
|
182
|
+
RESULT_FILE = "result.txt"
|
183
|
+
STATE_DIR_PREFIX = "state_"
|
184
|
+
|
185
|
+
|
186
|
+
def source_code_file state_dir
|
187
|
+
Dir.entries(state_dir).each { |file|
|
188
|
+
return state_file state_dir, file unless file =='..' || file == '.' ||file == RESULT_FILE }
|
189
|
+
end
|
190
|
+
|
191
|
+
def result_file state_dir
|
192
|
+
state_file state_dir, RESULT_FILE
|
193
|
+
end
|
194
|
+
|
195
|
+
def state_file state_dir, file
|
196
|
+
"#{state_dir}/#{file}"
|
197
|
+
end
|
198
|
+
|
199
|
+
def state_dir session_id, step
|
200
|
+
session_directory = session_dir session_id
|
201
|
+
"#{session_directory}/#{STATE_DIR_PREFIX}#{step}"
|
202
|
+
end
|
203
|
+
|
204
|
+
def session_dir session_id
|
205
|
+
"#{CODERSDOJO_WORKSPACE}/#{session_id}"
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
class Progress
|
211
|
+
|
212
|
+
def self.write_empty_progress states
|
213
|
+
STDOUT.print "#{states} states to upload"
|
214
|
+
STDOUT.print "["+" "*states+"]"
|
215
|
+
STDOUT.print "\b"*(states+1)
|
216
|
+
STDOUT.flush
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.next
|
220
|
+
STDOUT.print "."
|
221
|
+
STDOUT.flush
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.end
|
225
|
+
STDOUT.puts
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Uploader
|
230
|
+
|
231
|
+
def initialize hostname, framework, session_dir, state_reader = StateReader.new(Shell.new)
|
232
|
+
@hostname = hostname
|
233
|
+
@framework = framework
|
234
|
+
@state_reader = state_reader
|
235
|
+
@state_reader.session_id = session_dir.gsub('.codersdojo/', '')
|
236
|
+
end
|
237
|
+
|
238
|
+
def upload_kata
|
239
|
+
RestClient.post "#{@hostname}#{@@kata_path}", {:framework => @framework}
|
240
|
+
end
|
241
|
+
|
242
|
+
def upload_state kata_id
|
243
|
+
state = @state_reader.read_next_state
|
244
|
+
RestClient.post "#{@hostname}#{@@kata_path}/#{kata_id}#{@@state_path}", {:code => state.code, :result => state.result, :created_at => state.time}
|
245
|
+
Progress.next
|
246
|
+
end
|
247
|
+
|
248
|
+
def upload_states kata_id
|
249
|
+
Progress.write_empty_progress @state_reader.state_count
|
250
|
+
while @state_reader.has_next_state
|
251
|
+
upload_state kata_id
|
252
|
+
end
|
253
|
+
Progress.end
|
254
|
+
end
|
255
|
+
|
256
|
+
def upload_kata_and_states
|
257
|
+
kata = upload_kata
|
258
|
+
upload_states(XMLElementExtractor.extract('kata/id', kata))
|
259
|
+
"This is the link to review and comment your kata #{XMLElementExtractor.extract('kata/short-url', kata)}"
|
260
|
+
end
|
261
|
+
|
262
|
+
def upload
|
263
|
+
begin
|
264
|
+
require 'rest_client'
|
265
|
+
rescue LoadError
|
266
|
+
return 'Cant find gem rest-client. Please install it.'
|
267
|
+
end
|
268
|
+
return upload_kata_and_states if @state_reader.enough_states?
|
269
|
+
return "You need at least two states"
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
@@kata_path = '/katas'
|
274
|
+
@@state_path = '/states'
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
class XMLElementExtractor
|
279
|
+
def self.extract element, xml_string
|
280
|
+
doc = REXML::Document.new xml_string
|
281
|
+
return doc.elements.each(element) do |found|
|
282
|
+
return found.text
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class State
|
288
|
+
|
289
|
+
attr_accessor :time, :code, :result
|
290
|
+
|
291
|
+
def initialize time=nil, code=nil, result=nil
|
292
|
+
@time = time
|
293
|
+
@code = code
|
294
|
+
@result = result
|
295
|
+
end
|
296
|
+
|
297
|
+
end
|
298
|
+
|
299
|
+
class ArgumentParser
|
300
|
+
|
301
|
+
def initialize controller
|
302
|
+
@controller = controller
|
303
|
+
end
|
304
|
+
|
305
|
+
def parse params
|
306
|
+
command = params[0] ? params[0] : ""
|
307
|
+
if command.downcase == "help" then
|
308
|
+
@controller.help params[1]
|
309
|
+
elsif command.downcase == "show-examples" then
|
310
|
+
@controller.show_examples
|
311
|
+
elsif command.downcase == "setup" then
|
312
|
+
@controller.generate params[1], params[2] ? params[2] : '<kata_file>'
|
313
|
+
elsif command.downcase == "upload" then
|
314
|
+
@controller.upload params[1], params[2]
|
315
|
+
elsif command.downcase == "start" then
|
316
|
+
run_command = expand_run_command params[1]
|
317
|
+
@controller.start run_command, params[2]
|
318
|
+
elsif command.downcase == "spec" then
|
319
|
+
# 'spec" is for testing purpose only: do nothing special
|
320
|
+
else
|
321
|
+
raise ArgumentError
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def expand_run_command command
|
326
|
+
if command.end_with?(".sh") then
|
327
|
+
"bash #{command}"
|
328
|
+
elsif command.end_with?(".bat") or command.end_with?(".cmd") then
|
329
|
+
"start #{command}"
|
330
|
+
else
|
331
|
+
command
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
class Controller
|
338
|
+
|
339
|
+
def initialize view, hostname
|
340
|
+
@view = view
|
341
|
+
@hostname = hostname
|
342
|
+
end
|
343
|
+
|
344
|
+
def help command=nil
|
345
|
+
if command then
|
346
|
+
@view.show_detailed_help command.downcase
|
347
|
+
else
|
348
|
+
@view.show_help
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def show_examples
|
353
|
+
@view.show_examples
|
354
|
+
end
|
355
|
+
|
356
|
+
def generate framework, kata_file
|
357
|
+
generator = GeneratorFactory.new.create_generator framework
|
358
|
+
shell = Shell.new
|
359
|
+
generator_text = generator.generate kata_file
|
360
|
+
generator_text = shell.make_os_specific generator_text
|
361
|
+
puts generator_text
|
362
|
+
end
|
363
|
+
|
364
|
+
def start command, file
|
365
|
+
if not command or not file then
|
366
|
+
@view.show_missing_command_argument_error "start"
|
367
|
+
return
|
368
|
+
end
|
369
|
+
@view.show_start_kata command, file
|
370
|
+
dojo = Runner.new Shell.new, SessionIdGenerator.new
|
371
|
+
dojo.file = file
|
372
|
+
dojo.run_command = command
|
373
|
+
scheduler = Scheduler.new dojo
|
374
|
+
scheduler.start
|
375
|
+
end
|
376
|
+
|
377
|
+
def upload framework, session_directory
|
378
|
+
@view.show_upload_start @hostname
|
379
|
+
if session_directory then
|
380
|
+
uploader = Uploader.new @hostname, framework, session_directory
|
381
|
+
p uploader.upload
|
382
|
+
else
|
383
|
+
@view.show_missing_command_argument_error "upload"
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
end
|
388
|
+
|
389
|
+
|
390
|
+
class ConsoleView
|
391
|
+
|
392
|
+
@@VERSION = '0.9'
|
393
|
+
|
394
|
+
def show_help
|
395
|
+
puts <<-helptext
|
396
|
+
Personal CodersDojo, Version #{@@VERSION}, http://www.codersdojo.com, Copyright by it-agile GmbH (http://www.it-agile.de)
|
397
|
+
PersonalCodersDojo automatically runs your tests of a code kata.
|
398
|
+
|
399
|
+
helptext
|
400
|
+
show_usage
|
401
|
+
end
|
402
|
+
|
403
|
+
def show_usage
|
404
|
+
puts <<-helptext
|
405
|
+
Usage: #{$0} command [options]
|
406
|
+
Commands:
|
407
|
+
help, -h, --help Print this help text.
|
408
|
+
help <command> See the details of the command.
|
409
|
+
setup <framework> <kata_file> Setup the environment for running the kata.
|
410
|
+
upload <framework> <session_dir> Upload the kata to http://www.codersdojo.org
|
411
|
+
show-examples Show some example usages.
|
412
|
+
|
413
|
+
Report bugs to <codersdojo@it-agile.de>
|
414
|
+
helptext
|
415
|
+
end
|
416
|
+
|
417
|
+
def show_examples
|
418
|
+
puts <<-helptext
|
419
|
+
Examples:
|
420
|
+
:/dojo/my_kata% #{$0} setup ruby.test/unit prime
|
421
|
+
Show the instructions how to setup the environment for kata execution with ruby.
|
422
|
+
|
423
|
+
:/dojo/my_kata$ #{$0} start ruby prime.rb
|
424
|
+
Run the tests of prime.rb. The test runs automatically every second if prime.rb was modified.
|
425
|
+
|
426
|
+
:/dojo/my_kata$ #{$0} upload ruby.test/unit .codersdojo/2010-11-02_16-21-53
|
427
|
+
Upload the kata (written in Ruby with the test/unit framework) located in directory ".codersdojo/2010-11-02_16-21-53" to codersdojo.com.
|
428
|
+
helptext
|
429
|
+
end
|
430
|
+
|
431
|
+
def show_detailed_help command
|
432
|
+
if command == 'setup' then
|
433
|
+
show_help_setup
|
434
|
+
elsif command == 'start' then
|
435
|
+
show_help_start
|
436
|
+
elsif command == 'upload' then
|
437
|
+
show_help_upload
|
438
|
+
else
|
439
|
+
show_help_unknown command
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def show_help_setup
|
444
|
+
puts <<-helptext
|
445
|
+
setup <framework> <kata_file_no_ext> Setup the environment for the kata for the given framework and kata file.
|
446
|
+
The kata_file should not have an extension. Use 'prime' and not 'prime.java'.
|
447
|
+
By now <framework> is one of clojure.is-test, java.junit, javascript.jspec,
|
448
|
+
python.unittest, ruby.test/unit.
|
449
|
+
Use ??? as framework if your framework isn't in the list.
|
450
|
+
helptext
|
451
|
+
end
|
452
|
+
|
453
|
+
def show_help_start
|
454
|
+
puts <<-helptext
|
455
|
+
start <shell_command> <kata_file> Start the continuous test runner, that runs <shell-command> whenever <kata_file>
|
456
|
+
changes. The <kata_file> has to include the whole source code of the kata.
|
457
|
+
Whenever the test runner is started, it creates a new session directory in the
|
458
|
+
directory .codersdojo where it logs the steps of the kata.
|
459
|
+
helptext
|
460
|
+
end
|
461
|
+
|
462
|
+
def show_help_upload
|
463
|
+
puts <<-helptext
|
464
|
+
upload <framework> <session_directory> Upload the kata written with <framework> in <session_directory> to codersdojo.com.
|
465
|
+
<session_directory> is relative to the working directory.
|
466
|
+
Take a look at the generate command for the supported frameworks.
|
467
|
+
If you used another framework, use ??? and send an email to codersdojo@it-agile.de
|
468
|
+
helptext
|
469
|
+
end
|
470
|
+
|
471
|
+
def show_help_unknown command
|
472
|
+
puts <<-helptext
|
473
|
+
Command #{command} not known. Try '#{$0} help' to list the supported commands.
|
474
|
+
helptext
|
475
|
+
end
|
476
|
+
|
477
|
+
def show_start_kata command, file
|
478
|
+
puts "Starting PersonalCodersDojo with command #{command} and kata file #{file}. Use Ctrl+C to finish the kata."
|
479
|
+
end
|
480
|
+
|
481
|
+
def show_missing_command_argument_error command
|
482
|
+
puts "Command <#{command}> recognized but no argument was provided (at least one argument is required).\n\n"
|
483
|
+
show_usage
|
484
|
+
end
|
485
|
+
|
486
|
+
def show_upload_start hostname
|
487
|
+
puts "Start upload to #{hostname}"
|
488
|
+
end
|
489
|
+
|
490
|
+
def show_upload_result result
|
491
|
+
puts result
|
492
|
+
end
|
493
|
+
|
494
|
+
def show_socket_error command
|
495
|
+
puts "Encountered network error while <#{command}>. Is http://www.codersdojo.com down?"
|
496
|
+
end
|
497
|
+
|
498
|
+
end
|
499
|
+
|
500
|
+
class GeneratorFactory
|
501
|
+
|
502
|
+
def initialize
|
503
|
+
@frameworks = {"clojure.is-test" => ClosureGenerator,
|
504
|
+
"java.junit" => JavaGenerator,
|
505
|
+
"javascript.jspec" => JavascriptGenerator,
|
506
|
+
"python.unittest" => PythonGenerator,
|
507
|
+
"ruby.test/unit" => RubyGenerator}
|
508
|
+
end
|
509
|
+
|
510
|
+
def create_generator framework
|
511
|
+
generator_class = @frameworks[framework]
|
512
|
+
if generator_class.nil? then
|
513
|
+
generator_class = AnyGenerator
|
514
|
+
end
|
515
|
+
generator_class.new
|
516
|
+
end
|
517
|
+
|
518
|
+
end
|
519
|
+
|
520
|
+
class AnyGenerator
|
521
|
+
|
522
|
+
def generate kata_file
|
523
|
+
<<-generate_help
|
524
|
+
You have to create two shell scripts manually:
|
525
|
+
Create a shell script run-once.%sh% that runs the tests of your kata once.
|
526
|
+
|
527
|
+
Create a second shell script run-endless.sh with this content:
|
528
|
+
#{$0} start run-once.%sh% #{kata_file}.<extension>
|
529
|
+
|
530
|
+
Run run-endless.%sh% and start your kata.
|
531
|
+
|
532
|
+
Assumptions:
|
533
|
+
- The whole kata source code is in the one #{kata_file}.<extension>.
|
534
|
+
generate_help
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
class ClosureGenerator
|
539
|
+
def generate kata_file
|
540
|
+
<<-generate_help
|
541
|
+
You have to create two shell scripts manually:
|
542
|
+
Create a shell script run-once.%sh% with this content:
|
543
|
+
java -cp clojure-contrib.jar%:%clojure.jar clojure.main #{kata_file}.clj
|
544
|
+
|
545
|
+
Create a second shell script run-endless.sh with this content:
|
546
|
+
#{$0} start run-once.%sh% #{kata_file}.clj
|
547
|
+
|
548
|
+
Run run-endless.%sh% and start your kata.
|
549
|
+
|
550
|
+
Assumptions:
|
551
|
+
- Java is installed on your system and 'java' is in the path.
|
552
|
+
- clojure.jar and clojure-contrib.jar are placed in your work directory.
|
553
|
+
generate_help
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
class JavaGenerator
|
558
|
+
def generate kata_file
|
559
|
+
<<-generate_help
|
560
|
+
You have to create two shell scripts manually:
|
561
|
+
Create a shell script run-once.%sh% with this content:
|
562
|
+
%rm% bin/#{kata_file}.class
|
563
|
+
javac -cp lib/junit.jar -d bin #{kata_file}.java
|
564
|
+
java -cp lib/junit.jar%:%bin #{kata_file}
|
565
|
+
|
566
|
+
Create a second shell script run-endless.sh with this content:
|
567
|
+
#{$0} start run-once.%sh% src/#{kata_file}.java
|
568
|
+
|
569
|
+
Run run-endless.%sh% and start your kata.
|
570
|
+
|
571
|
+
Assumptions:
|
572
|
+
- A Java JDK is installed on your system and 'java' and 'javac' are in the path.
|
573
|
+
- junit.jar is placed in a directory named 'lib'.
|
574
|
+
- The kata source file is placed in the 'src' directory.
|
575
|
+
- The kata class file is generated into the 'class' directory.
|
576
|
+
- In the source file the classes are placed in the default package.
|
577
|
+
- The kata source file has a main method that starts the tests.
|
578
|
+
- If your IDE (like Eclipse) compiles the source file, you should remove the first two lines
|
579
|
+
in the run-once.%sh% file.
|
580
|
+
generate_help
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
|
585
|
+
class JavascriptGenerator
|
586
|
+
def generate kata_file
|
587
|
+
<<-generate_help
|
588
|
+
You have to create two shell scripts manually:
|
589
|
+
Create a shell script run-once.%sh% with this content:
|
590
|
+
jspec --rhino run
|
591
|
+
|
592
|
+
Create a second shell script run-endless.sh with this content:
|
593
|
+
#{$0} start run-once.%sh% #{kata_file}.js
|
594
|
+
|
595
|
+
Run run-endless.%sh% and start your kata.
|
596
|
+
generate_help
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
|
601
|
+
class PythonGenerator
|
602
|
+
def generate kata_file
|
603
|
+
<<-generate_help
|
604
|
+
You have to create two shell scripts manually:
|
605
|
+
Create a shell script run-once.%sh% with this content:
|
606
|
+
python #{kata_file}.py
|
607
|
+
|
608
|
+
Create a second shell script run-endless.sh with this content:
|
609
|
+
#{$0} start run-once.%sh% #{kata_file}.py
|
610
|
+
|
611
|
+
Run run-endless.%sh% and start your kata.
|
612
|
+
generate_help
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
|
617
|
+
class RubyGenerator
|
618
|
+
def generate kata_file
|
619
|
+
<<-generate_help
|
620
|
+
You have to create one shell script manually:
|
621
|
+
Create a shell script run-endless.%sh% with this content:
|
622
|
+
#{$0} start ruby #{kata_file}.rb
|
623
|
+
|
624
|
+
Run run-endless.%sh% and start your kata.
|
625
|
+
generate_help
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
def called_from_spec args
|
630
|
+
args[0] == "spec"
|
631
|
+
end
|
632
|
+
|
633
|
+
# entry from shell
|
634
|
+
if not called_from_spec(ARGV) then
|
635
|
+
view = ConsoleView.new
|
636
|
+
hostname = "http://www.codersdojo.com"
|
637
|
+
#hostname = "http://localhost:3000"
|
638
|
+
controller = Controller.new view, hostname
|
639
|
+
begin
|
640
|
+
arg_parser = ArgumentParser.new controller
|
641
|
+
command = arg_parser.parse ARGV
|
642
|
+
rescue ArgumentError
|
643
|
+
controller.help
|
644
|
+
end
|
645
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
ARGV[0] = "spec" # to be first line to suppress help text output of shell command
|
2
|
+
require 'rubygems'
|
3
|
+
require "app/personal_codersdojo"
|
4
|
+
require "restclient"
|
5
|
+
require "spec"
|
6
|
+
|
7
|
+
|
8
|
+
describe Runner, "in run mode" do
|
9
|
+
|
10
|
+
WORKSPACE_DIR = ".codersdojo"
|
11
|
+
SESSION_ID = "id0815"
|
12
|
+
SESSION_DIR = "#{WORKSPACE_DIR}/#{SESSION_ID}"
|
13
|
+
STATE_DIR_PREFIX = "#{SESSION_DIR}/state_"
|
14
|
+
|
15
|
+
before (:each) do
|
16
|
+
@shell_mock = mock.as_null_object
|
17
|
+
@session_id_provider_mock = mock.as_null_object
|
18
|
+
@session_id_provider_mock.should_receive(:generate_id).and_return SESSION_ID
|
19
|
+
@runner = Runner.new @shell_mock, @session_id_provider_mock
|
20
|
+
@runner.file = "my_file.rb"
|
21
|
+
@runner.run_command = "ruby"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should create codersdojo directory if it doesn't exist with session sub-directory" do
|
25
|
+
@shell_mock.should_receive(:mkdir_p).with SESSION_DIR
|
26
|
+
@runner.start
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should run ruby command on kata file given as argument" do
|
30
|
+
@shell_mock.should_receive(:execute).with "ruby my_file.rb"
|
31
|
+
@runner.start
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should create a state directory for every state" do
|
35
|
+
@shell_mock.should_receive(:ctime).with("my_file.rb").and_return 1
|
36
|
+
@shell_mock.should_receive(:mkdir).with "#{STATE_DIR_PREFIX}0"
|
37
|
+
@shell_mock.should_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}0"
|
38
|
+
@runner.start
|
39
|
+
@shell_mock.should_receive(:ctime).with("my_file.rb").and_return 2
|
40
|
+
@shell_mock.should_receive(:mkdir).with "#{STATE_DIR_PREFIX}1"
|
41
|
+
@shell_mock.should_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}1"
|
42
|
+
@runner.execute
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should not run if the kata file wasn't modified" do
|
46
|
+
a_time = Time.new
|
47
|
+
@shell_mock.should_receive(:ctime).with("my_file.rb").and_return a_time
|
48
|
+
@shell_mock.should_receive(:mkdir).with "#{STATE_DIR_PREFIX}0"
|
49
|
+
@shell_mock.should_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}0"
|
50
|
+
@runner.start
|
51
|
+
@shell_mock.should_receive(:ctime).with("my_file.rb").and_return a_time
|
52
|
+
@shell_mock.should_not_receive(:mkdir).with "#{STATE_DIR_PREFIX}1"
|
53
|
+
@shell_mock.should_not_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}1"
|
54
|
+
@runner.execute
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should capture run result into state directory" do
|
58
|
+
@shell_mock.should_receive(:execute).and_return "spec result"
|
59
|
+
@shell_mock.should_receive(:write_file).with "#{STATE_DIR_PREFIX}0/result.txt", "spec result"
|
60
|
+
@runner.start
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
describe SessionIdGenerator do
|
67
|
+
|
68
|
+
before (:each) do
|
69
|
+
@time_mock = mock
|
70
|
+
@generator = SessionIdGenerator.new
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should format id as yyyy-mm-dd_hh-mm-ss" do
|
74
|
+
@time_mock.should_receive(:year).and_return 2010
|
75
|
+
@time_mock.should_receive(:month).and_return 8
|
76
|
+
@time_mock.should_receive(:day).and_return 7
|
77
|
+
@time_mock.should_receive(:hour).and_return 6
|
78
|
+
@time_mock.should_receive(:min).and_return 5
|
79
|
+
@time_mock.should_receive(:sec).and_return 0
|
80
|
+
@generator.generate_id(@time_mock).should == "2010-08-07_06-05-00"
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
describe StateReader do
|
86
|
+
|
87
|
+
before (:each) do
|
88
|
+
@a_time = Time.new
|
89
|
+
@shell_mock = mock
|
90
|
+
@state_reader = StateReader.new @shell_mock
|
91
|
+
@state_reader.session_id = "id0815"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should read a stored kata state" do
|
95
|
+
@shell_mock.should_receive(:ctime).with("#{STATE_DIR_PREFIX}0").and_return @a_time
|
96
|
+
Dir.should_receive(:entries).with("#{STATE_DIR_PREFIX}0").and_return(['.','..','file.rb', 'result.txt'])
|
97
|
+
@shell_mock.should_receive(:read_file).with("#{STATE_DIR_PREFIX}0/file.rb").and_return "source code"
|
98
|
+
@shell_mock.should_receive(:read_file).with("#{STATE_DIR_PREFIX}0/result.txt").and_return "result"
|
99
|
+
state = @state_reader.read_next_state
|
100
|
+
state.time.should == @a_time
|
101
|
+
state.code.should == "source code"
|
102
|
+
state.result.should == "result"
|
103
|
+
@state_reader.next_step.should == 1
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
describe Uploader do
|
109
|
+
|
110
|
+
before (:each) do
|
111
|
+
@state_reader_mock = mock StateReader
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should convert session-dir to session-id" do
|
115
|
+
@state_reader_mock.should_receive(:session_id=).with("session_id")
|
116
|
+
Uploader.new "http://dummy_host", "dummy.framework", ".codersdojo/session_id", @state_reader_mock
|
117
|
+
end
|
118
|
+
|
119
|
+
context'upload' do
|
120
|
+
|
121
|
+
before (:each) do
|
122
|
+
@state_reader_mock = mock StateReader
|
123
|
+
@state_reader_mock.should_receive(:session_id=).with("path_to_kata")
|
124
|
+
@uploader = Uploader.new "http://dummy_host", "dummy.framework", "path_to_kata", @state_reader_mock
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should return error message if rest-client not installed" do
|
128
|
+
@uploader.stub(:require).and_raise(LoadError)
|
129
|
+
message = @uploader.upload
|
130
|
+
message.should == 'Cant find gem rest-client. Please install it.'
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should upload a kata through a rest-interface" do
|
134
|
+
RestClient.should_receive(:post).with('http://dummy_host/katas', {:framework => "dummy.framework"}).and_return '<id>222</id>'
|
135
|
+
@uploader.upload_kata
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should upload kata and states" do
|
139
|
+
@uploader.stub(:upload_kata).and_return 'kata_xml'
|
140
|
+
XMLElementExtractor.should_receive(:extract).with('kata/id', 'kata_xml').and_return 'kata_id'
|
141
|
+
@uploader.stub(:upload_states).with 'kata_id'
|
142
|
+
XMLElementExtractor.should_receive(:extract).with('kata/short-url', 'kata_xml').and_return 'short_url'
|
143
|
+
@uploader.upload_kata_and_states
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should upload if enugh states are there' do
|
147
|
+
@state_reader_mock.should_receive(:enough_states?).and_return 'true'
|
148
|
+
@uploader.stub!(:upload_kata_and_states).and_return 'kata_link'
|
149
|
+
@uploader.upload
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should return a helptext if not enught states are there' do
|
153
|
+
@state_reader_mock.should_receive(:enough_states?).and_return nil
|
154
|
+
help_text = @uploader.upload
|
155
|
+
help_text.should == 'You need at least two states'
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'states' do
|
159
|
+
it "should read all states and starts/ends progress" do
|
160
|
+
@state_reader_mock.should_receive(:state_count).and_return(1)
|
161
|
+
Progress.should_receive(:write_empty_progress).with(1)
|
162
|
+
|
163
|
+
@state_reader_mock.should_receive(:has_next_state).and_return 'true'
|
164
|
+
@uploader.should_receive(:upload_state)
|
165
|
+
@state_reader_mock.should_receive(:has_next_state).and_return nil
|
166
|
+
|
167
|
+
Progress.should_receive(:end)
|
168
|
+
|
169
|
+
@uploader.upload_states "kata_id"
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
it "through a rest interface and log process" do
|
174
|
+
state = mock State
|
175
|
+
@state_reader_mock.should_receive(:read_next_state).and_return state
|
176
|
+
state.should_receive(:code).and_return 'code'
|
177
|
+
state.should_receive(:time).and_return 'time'
|
178
|
+
state.should_receive(:result).and_return 'result'
|
179
|
+
RestClient.should_receive(:post).with('http://dummy_host/katas/kata_id/states', {:code=> 'code', :result => 'result', :created_at => 'time'})
|
180
|
+
Progress.should_receive(:next)
|
181
|
+
@uploader.upload_state "kata_id"
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
describe XMLElementExtractor do
|
191
|
+
|
192
|
+
it "should extract first element from a xml string" do
|
193
|
+
xmlString = "<?xml version='1.0' encoding='UTF-8'?>\n<kata>\n <created-at type='datetime'>2010-07-16T16:02:00+02:00</created-at>\n <end-date type='datetime' nil='true'/>\n <id type='integer'>60</id>\n <short-url nil='true'/>\n <updated-at type='datetime'>2010-07-16T16:02:00+02:00</updated-at>\n <uuid>2a5a83dc71b8ad6565bd99f15d01e41ec1a8f3f2</uuid>\n</kata>\n"
|
194
|
+
element = XMLElementExtractor.extract 'kata/id', xmlString
|
195
|
+
element.should == "60"
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
describe ArgumentParser do
|
201
|
+
|
202
|
+
before (:each) do
|
203
|
+
@controller_mock = mock.as_null_object
|
204
|
+
@parser = ArgumentParser.new @controller_mock
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should reject empty command" do
|
208
|
+
lambda{@parser.parse []}.should raise_error
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should reject unknown command" do
|
212
|
+
lambda{@parser.parse "unknown command"}.should raise_error
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should accept help command" do
|
216
|
+
@controller_mock.should_receive(:help).with(nil)
|
217
|
+
@parser.parse ["help"]
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should accept start command" do
|
221
|
+
@controller_mock.should_receive(:start).with "aCommand", "aFile"
|
222
|
+
@parser.parse ["start", "aCommand","aFile"]
|
223
|
+
end
|
224
|
+
|
225
|
+
it "should prepend *.sh start scripts with 'bash'" do
|
226
|
+
@controller_mock.should_receive(:start).with "bash aCommand.sh", "aFile"
|
227
|
+
@parser.parse ["start", "aCommand.sh","aFile"]
|
228
|
+
end
|
229
|
+
|
230
|
+
it "should prepend *.bat start scripts with 'start'" do
|
231
|
+
@controller_mock.should_receive(:start).with "start aCommand.bat", "aFile"
|
232
|
+
@parser.parse ["start", "aCommand.bat","aFile"]
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should prepend *.cmd start scripts with 'start'" do
|
236
|
+
@controller_mock.should_receive(:start).with "start aCommand.cmd", "aFile"
|
237
|
+
@parser.parse ["start", "aCommand.cmd","aFile"]
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should accept upload command" do
|
241
|
+
@controller_mock.should_receive(:upload).with "framework", "dir"
|
242
|
+
@parser.parse ["upload", "framework", "dir"]
|
243
|
+
end
|
244
|
+
|
245
|
+
it "should accept uppercase commands" do
|
246
|
+
@controller_mock.should_receive(:help).with(nil)
|
247
|
+
@parser.parse ["HELP"]
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
describe Progress do
|
253
|
+
|
254
|
+
it 'should print infos and empty progress in initialization' do
|
255
|
+
STDOUT.should_receive(:print).with("2 states to upload")
|
256
|
+
STDOUT.should_receive(:print).with("[ ]")
|
257
|
+
STDOUT.should_receive(:print).with("\b\b\b")
|
258
|
+
Progress.write_empty_progress 2
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'should print dots and flush in next' do
|
262
|
+
STDOUT.should_receive(:print).with(".")
|
263
|
+
STDOUT.should_receive(:flush)
|
264
|
+
Progress.next
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'should print empty line in end' do
|
268
|
+
STDOUT.should_receive(:puts)
|
269
|
+
Progress.end
|
270
|
+
end
|
271
|
+
|
272
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: codersdojo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
- 0
|
9
|
+
version: 0.9.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- CodersDojo-Team
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-12-10 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Client executes tests in an endless loop and logs source code and test result.
|
22
|
+
email: codersdojo@it-agile.de
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- app/personal_codersdojo.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://www.codersdojo.org/
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project: codersdojo
|
57
|
+
rubygems_version: 1.3.6
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Client for CodersDojo.org
|
61
|
+
test_files:
|
62
|
+
- spec/personal_codersdojo_spec.rb
|