codersdojo 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,108 @@
1
+ require 'shell_wrapper'
2
+
3
+ class ConsoleView
4
+
5
+ @@VERSION = '0.9'
6
+
7
+ def show_help
8
+ puts <<-helptext
9
+ Personal CodersDojo, Version #{@@VERSION}, http://www.codersdojo.com, Copyright by it-agile GmbH (http://www.it-agile.de)
10
+ PersonalCodersDojo automatically runs your tests of a code kata.
11
+
12
+ helptext
13
+ show_usage
14
+ end
15
+
16
+ def show_usage
17
+ puts <<-helptext
18
+ Usage: #{$0} command [options]
19
+ Commands:
20
+ help, -h, --help Print this help text.
21
+ help <command> See the details of the command.
22
+ setup <framework> <kata_file> Setup the environment for running the kata.
23
+ upload <framework> <session_dir> Upload the kata to http://www.codersdojo.org
24
+
25
+ Report bugs to <codersdojo@it-agile.de>
26
+ helptext
27
+ end
28
+
29
+ def show_detailed_help command
30
+ if command == 'setup' then
31
+ show_help_setup
32
+ elsif command == 'start' then
33
+ show_help_start
34
+ elsif command == 'upload' then
35
+ show_help_upload
36
+ else
37
+ show_help_unknown command
38
+ end
39
+ end
40
+
41
+ def show_help_setup
42
+ templates = ShellWrapper.new.list_templates
43
+ puts <<-helptext
44
+
45
+ setup <framework> <kata_file_no_ext> Setup the environment for the kata for the given framework and kata file.
46
+ The kata_file should not have an extension. Use 'prime' and not 'prime.java'.
47
+ By now <framework> is one of #{templates}.
48
+ Use ??? as framework if your framework isn't in the list.
49
+
50
+ Example:
51
+ :/dojo/my_kata% #{$0} setup ruby.test/unit prime
52
+ Show the instructions how to setup the environment for kata execution with Ruby and test/unit.
53
+ helptext
54
+ end
55
+
56
+ def show_help_start
57
+ puts <<-helptext
58
+
59
+ start <shell_command> <kata_file> Start the continuous test runner, that runs <shell-command> whenever <kata_file>
60
+ changes. The <kata_file> has to include the whole source code of the kata.
61
+ Whenever the test runner is started, it creates a new session directory in the
62
+ directory .codersdojo where it logs the steps of the kata.
63
+ helptext
64
+ end
65
+
66
+ def show_help_upload
67
+ templates = ShellWrapper.new.list_templates
68
+ puts <<-helptext
69
+
70
+ upload <framework> <session_directory> Upload the kata written with <framework> in <session_directory> to codersdojo.com.
71
+ <session_directory> is relative to the working directory.
72
+ By now <framework> is one of #{templates}.
73
+ If you used another framework, use ??? and send an email to codersdojo@it-agile.de
74
+
75
+ Example:
76
+ :/dojo/my_kata$ #{$0} upload ruby.test/unit .codersdojo/2010-11-02_16-21-53
77
+ Upload the kata (written in Ruby with the test/unit framework) located in directory ".codersdojo/2010-11-02_16-21-53" to codersdojo.com.
78
+ helptext
79
+ end
80
+
81
+ def show_help_unknown command
82
+ puts <<-helptext
83
+ Command #{command} not known. Try '#{$0} help' to list the supported commands.
84
+ helptext
85
+ end
86
+
87
+ def show_start_kata command, file
88
+ puts "Starting PersonalCodersDojo with command #{command} and kata file #{file}. Use Ctrl+C to finish the kata."
89
+ end
90
+
91
+ def show_missing_command_argument_error command
92
+ puts "Command <#{command}> recognized but no argument was provided (at least one argument is required).\n\n"
93
+ show_usage
94
+ end
95
+
96
+ def show_upload_start hostname
97
+ puts "Start upload to #{hostname}"
98
+ end
99
+
100
+ def show_upload_result result
101
+ puts result
102
+ end
103
+
104
+ def show_socket_error command
105
+ puts "Encountered network error while <#{command}>. Is http://www.codersdojo.com down?"
106
+ end
107
+
108
+ end
data/app/controller.rb ADDED
@@ -0,0 +1,50 @@
1
+ class Controller
2
+
3
+ def initialize view, hostname
4
+ @view = view
5
+ @hostname = hostname
6
+ end
7
+
8
+ def help command=nil
9
+ if command then
10
+ @view.show_detailed_help command.downcase
11
+ else
12
+ @view.show_help
13
+ end
14
+ end
15
+
16
+ def generate framework, kata_file
17
+ shell = ShellWrapper.new
18
+ shell.scaffold framework
19
+ generator_text = IO.readlines("README").to_s;
20
+ generator_text = generator_text.gsub('#{kata_file}', kata_file)
21
+ generator_text = generator_text.gsub('#{$0}', $0)
22
+ generator_text = shell.make_os_specific generator_text
23
+ puts "\n" + generator_text
24
+ end
25
+
26
+ def start command, file
27
+ if not command or not file then
28
+ @view.show_missing_command_argument_error "start"
29
+ return
30
+ end
31
+ @view.show_start_kata command, file
32
+ dojo = Runner.new ShellWrapper.new, SessionIdGenerator.new
33
+ dojo.file = file
34
+ dojo.run_command = command
35
+ scheduler = Scheduler.new dojo
36
+ scheduler.start
37
+ end
38
+
39
+ def upload framework, session_directory
40
+ @view.show_upload_start @hostname
41
+ if session_directory then
42
+ uploader = Uploader.new @hostname, framework, session_directory
43
+ p uploader.upload
44
+ else
45
+ @view.show_missing_command_argument_error "upload"
46
+ end
47
+ end
48
+
49
+ end
50
+
@@ -0,0 +1,34 @@
1
+ class FilenameFormatter
2
+
3
+ CODERSDOJO_WORKSPACE = ".codersdojo"
4
+ RESULT_FILE = "result.txt"
5
+ STATE_DIR_PREFIX = "state_"
6
+
7
+ def self.state_dir_prefix
8
+ STATE_DIR_PREFIX
9
+ end
10
+
11
+ def source_code_file state_dir
12
+ Dir.entries(state_dir).each { |file|
13
+ return state_file state_dir, file unless file =='..' || file == '.' ||file == RESULT_FILE }
14
+ end
15
+
16
+ def result_file state_dir
17
+ state_file state_dir, RESULT_FILE
18
+ end
19
+
20
+ def state_file state_dir, file
21
+ "#{state_dir}/#{file}"
22
+ end
23
+
24
+ def state_dir session_id, step
25
+ session_directory = session_dir session_id
26
+ "#{session_directory}/#{STATE_DIR_PREFIX}#{step}"
27
+ end
28
+
29
+ def session_dir session_id
30
+ "#{CODERSDOJO_WORKSPACE}/#{session_id}"
31
+ end
32
+
33
+ end
34
+
data/app/progress.rb ADDED
@@ -0,0 +1,19 @@
1
+ class Progress
2
+
3
+ def self.write_empty_progress states
4
+ STDOUT.print "#{states} states to upload"
5
+ STDOUT.print "["+" "*states+"]"
6
+ STDOUT.print "\b"*(states+1)
7
+ STDOUT.flush
8
+ end
9
+
10
+ def self.next
11
+ STDOUT.print "."
12
+ STDOUT.flush
13
+ end
14
+
15
+ def self.end
16
+ STDOUT.puts
17
+ end
18
+ end
19
+
data/app/runner.rb ADDED
@@ -0,0 +1,39 @@
1
+ require "filename_formatter"
2
+
3
+ class Runner
4
+
5
+ attr_accessor :file, :run_command
6
+
7
+ def initialize shell, session_provider
8
+ @filename_formatter = FilenameFormatter.new
9
+ @shell = shell
10
+ @session_provider = session_provider
11
+ end
12
+
13
+ def start
14
+ init_session
15
+ execute
16
+ end
17
+
18
+ def init_session
19
+ @step = 0
20
+ @session_id = @session_provider.generate_id
21
+ @shell.mkdir_p(@filename_formatter.session_dir @session_id)
22
+ end
23
+
24
+ def execute
25
+ change_time = @shell.ctime @file
26
+ if change_time == @previous_change_time then
27
+ return
28
+ end
29
+ result = @shell.execute "#{@run_command} #{@file}"
30
+ state_dir = @filename_formatter.state_dir @session_id, @step
31
+ @shell.mkdir state_dir
32
+ @shell.cp @file, state_dir
33
+ @shell.write_file @filename_formatter.result_file(state_dir), result
34
+ @step += 1
35
+ @previous_change_time = change_time
36
+ end
37
+
38
+ end
39
+
data/app/scheduler.rb ADDED
@@ -0,0 +1,15 @@
1
+ class Scheduler
2
+
3
+ def initialize runner
4
+ @runner = runner
5
+ end
6
+
7
+ def start
8
+ @runner.start
9
+ while true do
10
+ sleep 1
11
+ @runner.execute
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,17 @@
1
+ class SessionIdGenerator
2
+
3
+ def generate_id time=Time.new
4
+ year = format_to_length time.year, 4
5
+ month = format_to_length time.month, 2
6
+ day = format_to_length time.day, 2
7
+ hour = format_to_length time.hour, 2
8
+ minute = format_to_length time.min, 2
9
+ second = format_to_length time.sec, 2
10
+ "#{year}-#{month}-#{day}_#{hour}-#{minute}-#{second}"
11
+ end
12
+
13
+ def format_to_length value, len
14
+ value.to_s.rjust len,"0"
15
+ end
16
+
17
+ end
@@ -0,0 +1,96 @@
1
+ require 'tempfile'
2
+
3
+ class ShellWrapper
4
+
5
+ MAX_STDOUT_LENGTH = 100000
6
+ ANY_TEMPLATE = "any"
7
+
8
+ def cp source, destination
9
+ FileUtils.cp source, destination
10
+ end
11
+
12
+ def mkdir dir
13
+ FileUtils.mkdir dir
14
+ end
15
+
16
+ def mkdir_p dirs
17
+ FileUtils.mkdir_p dirs
18
+ end
19
+
20
+ def execute command
21
+ spec_pipe = IO.popen(command, "r")
22
+ result = spec_pipe.read MAX_STDOUT_LENGTH
23
+ spec_pipe.close
24
+ puts result
25
+ result
26
+ end
27
+
28
+ def write_file filename, content
29
+ file = File.new filename, "w"
30
+ file << content
31
+ file.close
32
+ end
33
+
34
+ def read_file filename
35
+ file = File.new filename
36
+ content = file.read
37
+ file.close
38
+ content
39
+ end
40
+
41
+ def ctime filename
42
+ File.new(filename).ctime
43
+ end
44
+
45
+ def list_templates
46
+ templates = real_dir_entries template_path
47
+ templates.delete ANY_TEMPLATE
48
+ templates.join(', ')
49
+ end
50
+
51
+ def scaffold template
52
+ begin
53
+ dir_path = "#{template_path}/#{template}"
54
+ to_copy = real_dir_entries dir_path
55
+ rescue
56
+ dir_path = "#{template_path}/#{ANY_TEMPLATE}"
57
+ to_copy = real_dir_entries dir_path
58
+ end
59
+ to_copy.each do |item|
60
+ FileUtils.cp_r "#{dir_path}/#{item}", "."
61
+ end
62
+ end
63
+
64
+ def template_path
65
+ file_path_elements = __FILE__.split '/'
66
+ file_path_elements[-2..-1] = nil
67
+ (file_path_elements << 'templates').join '/'
68
+ end
69
+
70
+ def real_dir_entries dir
71
+ current_and_parent = 2
72
+ Dir.new(dir).entries.drop current_and_parent
73
+ end
74
+
75
+ def make_os_specific text
76
+ text.gsub('%sh%', shell_extension).gsub('%:%', path_separator).gsub('%rm%', remove_command_name)
77
+ end
78
+
79
+ def remove_command_name
80
+ windows? ? 'delete' : 'rm'
81
+ end
82
+
83
+ def shell_extension
84
+ windows? ? 'cmd' : 'sh'
85
+ end
86
+
87
+ def path_separator
88
+ windows? ? ';' : ':'
89
+ end
90
+
91
+ def windows?
92
+ RUBY_PLATFORM.downcase.include? "windows"
93
+ end
94
+
95
+ end
96
+
@@ -0,0 +1,57 @@
1
+ require 'filename_formatter'
2
+
3
+ class StateReader
4
+
5
+ attr_accessor :session_id, :next_step
6
+
7
+ def initialize shell
8
+ @filename_formatter = FilenameFormatter.new
9
+ @shell = shell
10
+ @next_step = 0
11
+ end
12
+
13
+ def state_count
14
+ dummy_dirs_current_and_parent = 2
15
+ Dir.new(@filename_formatter.session_dir @session_id).count - dummy_dirs_current_and_parent
16
+ end
17
+
18
+ def enough_states?
19
+ state_count >= states_needed_for_one_move
20
+ end
21
+
22
+ def states_needed_for_one_move
23
+ 2
24
+ end
25
+
26
+ def get_state_dir
27
+ @filename_formatter.state_dir(@session_id, @next_step)
28
+ end
29
+
30
+ def has_next_state
31
+ File.exist?(get_state_dir)
32
+ end
33
+
34
+ def read_next_state
35
+ state = State.new
36
+ state_dir = get_state_dir
37
+ state.time = @shell.ctime state_dir
38
+ state.code = @shell.read_file @filename_formatter.source_code_file(state_dir)
39
+ state.result = @shell.read_file @filename_formatter.result_file(state_dir)
40
+ @next_step += 1
41
+ state
42
+ end
43
+
44
+ end
45
+
46
+ class State
47
+
48
+ attr_accessor :time, :code, :result
49
+
50
+ def initialize time=nil, code=nil, result=nil
51
+ @time = time
52
+ @code = code
53
+ @result = result
54
+ end
55
+
56
+ end
57
+
data/app/uploader.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'state_reader'
2
+ require 'progress'
3
+ require 'xml_element_extractor'
4
+ require 'rest_client'
5
+
6
+ class Uploader
7
+
8
+ def initialize hostname, framework, session_dir, state_reader = StateReader.new(ShellWrapper.new)
9
+ @hostname = hostname
10
+ @framework = framework
11
+ @state_reader = state_reader
12
+ @state_reader.session_id = session_dir.gsub('.codersdojo/', '')
13
+ end
14
+
15
+ def upload_kata
16
+ RestClient.post "#{@hostname}#{@@kata_path}", {:framework => @framework}
17
+ end
18
+
19
+ def upload_state kata_id
20
+ state = @state_reader.read_next_state
21
+ RestClient.post "#{@hostname}#{@@kata_path}/#{kata_id}#{@@state_path}", {:code => state.code, :result => state.result, :created_at => state.time}
22
+ Progress.next
23
+ end
24
+
25
+ def upload_states kata_id
26
+ Progress.write_empty_progress @state_reader.state_count
27
+ while @state_reader.has_next_state
28
+ upload_state kata_id
29
+ end
30
+ Progress.end
31
+ end
32
+
33
+ def upload_kata_and_states
34
+ kata = upload_kata
35
+ upload_states(XMLElementExtractor.extract('kata/id', kata))
36
+ "This is the link to review and comment your kata #{XMLElementExtractor.extract('kata/short-url', kata)}"
37
+ end
38
+
39
+ def upload
40
+ return upload_kata_and_states if @state_reader.enough_states?
41
+ return "You need at least two states"
42
+ end
43
+
44
+ private
45
+ @@kata_path = '/katas'
46
+ @@state_path = '/states'
47
+
48
+ end
49
+
@@ -0,0 +1,11 @@
1
+ require "rexml/document"
2
+
3
+ class XMLElementExtractor
4
+ def self.extract element, xml_string
5
+ doc = REXML::Document.new xml_string
6
+ return doc.elements.each(element) do |found|
7
+ return found.text
8
+ end
9
+ end
10
+ end
11
+
data/bin/codersdojo CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  codersdojo_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'app'))
3
+ infrastructure_dir = "#{codersdojo_dir}/infrastructure"
4
+ models_dir = "#{codersdojo_dir}/models"
3
5
  $LOAD_PATH.unshift(codersdojo_dir) unless $LOAD_PATH.include?(codersdojo_dir)
6
+ $LOAD_PATH.unshift(infrastructure_dir) unless $LOAD_PATH.include?(infrastructure_dir)
7
+ $LOAD_PATH.unshift(models_dir) unless $LOAD_PATH.include?(models_dir)
4
8
  require 'codersdojo'
@@ -0,0 +1,54 @@
1
+ require 'argument_parser'
2
+
3
+ describe ArgumentParser do
4
+
5
+ before (:each) do
6
+ @controller_mock = mock.as_null_object
7
+ @parser = ArgumentParser.new @controller_mock
8
+ end
9
+
10
+ it "should reject empty command" do
11
+ lambda{@parser.parse []}.should raise_error
12
+ end
13
+
14
+ it "should reject unknown command" do
15
+ lambda{@parser.parse "unknown command"}.should raise_error
16
+ end
17
+
18
+ it "should accept help command" do
19
+ @controller_mock.should_receive(:help).with(nil)
20
+ @parser.parse ["help"]
21
+ end
22
+
23
+ it "should accept start command" do
24
+ @controller_mock.should_receive(:start).with "aCommand", "aFile"
25
+ @parser.parse ["start", "aCommand","aFile"]
26
+ end
27
+
28
+ it "should prepend *.sh start scripts with 'bash'" do
29
+ @controller_mock.should_receive(:start).with "bash aCommand.sh", "aFile"
30
+ @parser.parse ["start", "aCommand.sh","aFile"]
31
+ end
32
+
33
+ it "should prepend *.bat start scripts with 'start'" do
34
+ @controller_mock.should_receive(:start).with "start aCommand.bat", "aFile"
35
+ @parser.parse ["start", "aCommand.bat","aFile"]
36
+ end
37
+
38
+ it "should prepend *.cmd start scripts with 'start'" do
39
+ @controller_mock.should_receive(:start).with "start aCommand.cmd", "aFile"
40
+ @parser.parse ["start", "aCommand.cmd","aFile"]
41
+ end
42
+
43
+ it "should accept upload command" do
44
+ @controller_mock.should_receive(:upload).with "framework", "dir"
45
+ @parser.parse ["upload", "framework", "dir"]
46
+ end
47
+
48
+ it "should accept uppercase commands" do
49
+ @controller_mock.should_receive(:help).with(nil)
50
+ @parser.parse ["HELP"]
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,23 @@
1
+ require 'progress'
2
+
3
+ describe Progress do
4
+
5
+ it 'should print infos and empty progress in initialization' do
6
+ STDOUT.should_receive(:print).with("2 states to upload")
7
+ STDOUT.should_receive(:print).with("[ ]")
8
+ STDOUT.should_receive(:print).with("\b\b\b")
9
+ Progress.write_empty_progress 2
10
+ end
11
+
12
+ it 'should print dots and flush in next' do
13
+ STDOUT.should_receive(:print).with(".")
14
+ STDOUT.should_receive(:flush)
15
+ Progress.next
16
+ end
17
+
18
+ it 'should print empty line in end' do
19
+ STDOUT.should_receive(:puts)
20
+ Progress.end
21
+ end
22
+
23
+ end
@@ -0,0 +1,59 @@
1
+ require 'runner'
2
+
3
+ describe Runner, "in run mode" do
4
+
5
+ WORKSPACE_DIR = ".codersdojo"
6
+ SESSION_ID = "id0815"
7
+ SESSION_DIR = "#{WORKSPACE_DIR}/#{SESSION_ID}"
8
+ STATE_DIR_PREFIX = "#{SESSION_DIR}/state_"
9
+
10
+ before (:each) do
11
+ @shell_mock = mock.as_null_object
12
+ @session_id_provider_mock = mock.as_null_object
13
+ @session_id_provider_mock.should_receive(:generate_id).and_return SESSION_ID
14
+ @runner = Runner.new @shell_mock, @session_id_provider_mock
15
+ @runner.file = "my_file.rb"
16
+ @runner.run_command = "ruby"
17
+ end
18
+
19
+ it "should create codersdojo directory if it doesn't exist with session sub-directory" do
20
+ @shell_mock.should_receive(:mkdir_p).with SESSION_DIR
21
+ @runner.start
22
+ end
23
+
24
+ it "should run ruby command on kata file given as argument" do
25
+ @shell_mock.should_receive(:execute).with "ruby my_file.rb"
26
+ @runner.start
27
+ end
28
+
29
+ it "should create a state directory for every state" do
30
+ @shell_mock.should_receive(:ctime).with("my_file.rb").and_return 1
31
+ @shell_mock.should_receive(:mkdir).with "#{STATE_DIR_PREFIX}0"
32
+ @shell_mock.should_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}0"
33
+ @runner.start
34
+ @shell_mock.should_receive(:ctime).with("my_file.rb").and_return 2
35
+ @shell_mock.should_receive(:mkdir).with "#{STATE_DIR_PREFIX}1"
36
+ @shell_mock.should_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}1"
37
+ @runner.execute
38
+ end
39
+
40
+ it "should not run if the kata file wasn't modified" do
41
+ a_time = Time.new
42
+ @shell_mock.should_receive(:ctime).with("my_file.rb").and_return a_time
43
+ @shell_mock.should_receive(:mkdir).with "#{STATE_DIR_PREFIX}0"
44
+ @shell_mock.should_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}0"
45
+ @runner.start
46
+ @shell_mock.should_receive(:ctime).with("my_file.rb").and_return a_time
47
+ @shell_mock.should_not_receive(:mkdir).with "#{STATE_DIR_PREFIX}1"
48
+ @shell_mock.should_not_receive(:cp).with "my_file.rb", "#{STATE_DIR_PREFIX}1"
49
+ @runner.execute
50
+ end
51
+
52
+ it "should capture run result into state directory" do
53
+ @shell_mock.should_receive(:execute).and_return "spec result"
54
+ @shell_mock.should_receive(:write_file).with "#{STATE_DIR_PREFIX}0/result.txt", "spec result"
55
+ @runner.start
56
+ end
57
+
58
+ end
59
+
@@ -0,0 +1,21 @@
1
+ require 'session_id_generator'
2
+
3
+ describe SessionIdGenerator do
4
+
5
+ before (:each) do
6
+ @time_mock = mock
7
+ @generator = SessionIdGenerator.new
8
+ end
9
+
10
+ it "should format id as yyyy-mm-dd_hh-mm-ss" do
11
+ @time_mock.should_receive(:year).and_return 2010
12
+ @time_mock.should_receive(:month).and_return 8
13
+ @time_mock.should_receive(:day).and_return 7
14
+ @time_mock.should_receive(:hour).and_return 6
15
+ @time_mock.should_receive(:min).and_return 5
16
+ @time_mock.should_receive(:sec).and_return 0
17
+ @generator.generate_id(@time_mock).should == "2010-08-07_06-05-00"
18
+ end
19
+
20
+ end
21
+