codersdojo 0.9.0
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/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
|