citrus-core 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/.citrus/config.rb +1 -1
- data/.gitignore +2 -1
- data/.travis.yml +4 -5
- data/README.md +1 -1
- data/Rakefile +3 -6
- data/citrus-core.gemspec +4 -2
- data/examples/bootstrap.rb +17 -17
- data/examples/payload.json +128 -1
- data/lib/citrus/core.rb +4 -18
- data/lib/citrus/core/build.rb +4 -2
- data/lib/citrus/core/cached_code_fetcher.rb +6 -5
- data/lib/citrus/core/configuration_loader.rb +2 -2
- data/lib/citrus/core/execute_build.rb +42 -0
- data/lib/citrus/core/exit_code.rb +21 -0
- data/lib/citrus/core/result.rb +7 -0
- data/lib/citrus/core/test_output.rb +19 -0
- data/lib/citrus/core/test_runner.rb +4 -6
- data/lib/citrus/core/version.rb +1 -1
- data/lib/citrus/core/workspace_builder.rb +5 -5
- data/lib/citrus/core/world.rb +21 -0
- data/spec/citrus/core/build_spec.rb +16 -0
- data/spec/{cached_code_fetcher_spec.rb → citrus/core/cached_code_fetcher_spec.rb} +17 -16
- data/spec/{changeset_spec.rb → citrus/core/changeset_spec.rb} +0 -0
- data/spec/citrus/core/clean_ruby_spec.rb +17 -0
- data/spec/{commit_changes_spec.rb → citrus/core/commit_changes_spec.rb} +0 -0
- data/spec/{commit_spec.rb → citrus/core/commit_spec.rb} +0 -0
- data/spec/{cofiguration_loader_spec.rb → citrus/core/configuration_loader_spec.rb} +1 -1
- data/spec/{cofiguration_spec.rb → citrus/core/configuration_spec.rb} +0 -0
- data/spec/{cofiguration_validator_spec.rb → citrus/core/configuration_validator_spec.rb} +0 -0
- data/spec/{execute_build_service_spec.rb → citrus/core/execute_build_spec.rb} +20 -15
- data/spec/{test_result_spec.rb → citrus/core/exit_code_spec.rb} +7 -6
- data/spec/{github_adapter_spec.rb → citrus/core/github_adapter_spec.rb} +1 -1
- data/spec/{publisher_spec.rb → citrus/core/publisher_spec.rb} +1 -1
- data/spec/{repository_spec.rb → citrus/core/repository_spec.rb} +0 -0
- data/spec/citrus/core/test_output_spec.rb +16 -0
- data/spec/citrus/core/test_runner_spec.rb +55 -0
- data/spec/citrus/core/workspace_builder_spec.rb +37 -0
- data/spec/citrus/core/world_spec.rb +11 -0
- data/spec/spec_helper.rb +6 -2
- metadata +81 -47
- data/examples/web.rb +0 -53
- data/lib/citrus/core/execute_build_service.rb +0 -30
- data/lib/citrus/core/test_result.rb +0 -24
- data/spec/build_spec.rb +0 -13
- data/spec/citrus_spec.rb +0 -18
- data/spec/test_runner_spec.rb +0 -44
- data/spec/workspace_builder_spec.rb +0 -55
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'stringio'
|
2
|
-
|
3
1
|
module Citrus
|
4
2
|
module Core
|
5
3
|
class TestRunner
|
@@ -7,24 +5,24 @@ module Citrus
|
|
7
5
|
|
8
6
|
include Publisher
|
9
7
|
|
10
|
-
def start(configuration, path)
|
8
|
+
def start(build, configuration, path)
|
9
|
+
output = build.output
|
11
10
|
process = ChildProcess.build(configuration.build_script)
|
12
11
|
process.cwd = path.to_s
|
13
12
|
r, w = IO.pipe
|
14
13
|
process.io.stdout = process.io.stderr = w
|
15
14
|
process.start
|
16
15
|
w.close
|
17
|
-
output = StringIO.new
|
18
16
|
begin
|
19
17
|
loop do
|
20
18
|
chunk = r.readpartial(CHUNK_SIZE)
|
21
19
|
output.write(chunk)
|
22
|
-
publish(:
|
20
|
+
publish(:build_output_received, build, chunk)
|
23
21
|
end
|
24
22
|
rescue EOFError
|
25
23
|
end
|
26
24
|
process.wait
|
27
|
-
|
25
|
+
ExitCode.new(process.exit_code)
|
28
26
|
end
|
29
27
|
|
30
28
|
end
|
data/lib/citrus/core/version.rb
CHANGED
@@ -2,16 +2,16 @@ module Citrus
|
|
2
2
|
module Core
|
3
3
|
class WorkspaceBuilder
|
4
4
|
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :build_root, :code_fetcher
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
7
|
+
def initialize(build_root, code_fetcher)
|
8
|
+
@build_root = build_root
|
9
9
|
@code_fetcher = code_fetcher
|
10
10
|
end
|
11
11
|
|
12
12
|
def create_workspace(build)
|
13
|
-
path =
|
14
|
-
|
13
|
+
path = File.join(build_root, partition, build.uuid)
|
14
|
+
FileUtils.mkpath(path)
|
15
15
|
code_fetcher.fetch(build.changeset, path)
|
16
16
|
path
|
17
17
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Citrus
|
2
|
+
module Core
|
3
|
+
class World
|
4
|
+
|
5
|
+
attr_reader :root
|
6
|
+
|
7
|
+
def initialize(root)
|
8
|
+
@root = root
|
9
|
+
end
|
10
|
+
|
11
|
+
def cache_root
|
12
|
+
File.join(root, 'cache')
|
13
|
+
end
|
14
|
+
|
15
|
+
def build_root
|
16
|
+
File.join(root, 'builds')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
describe Citrus::Core::Build do
|
5
|
+
|
6
|
+
subject { described_class.new(changeset, uuid, output) }
|
7
|
+
|
8
|
+
let(:changeset) { fake(:changeset) }
|
9
|
+
let(:uuid) { SecureRandom.uuid }
|
10
|
+
let(:output) { fake(:test_output) }
|
11
|
+
|
12
|
+
specify { expect(subject).to respond_to(:changeset) }
|
13
|
+
specify { expect(subject).to respond_to(:uuid) }
|
14
|
+
specify { expect(subject).to respond_to(:output) }
|
15
|
+
|
16
|
+
end
|
@@ -1,44 +1,47 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
class Digester
|
5
|
+
def hexdigest(input); input; end
|
6
|
+
end
|
2
7
|
|
3
8
|
describe Citrus::Core::CachedCodeFetcher do
|
4
9
|
|
5
|
-
subject { described_class.new(cache_root, vcs_adapter) }
|
10
|
+
subject { described_class.new(cache_root, vcs_adapter, digester) }
|
6
11
|
|
7
|
-
let(:destination) {
|
12
|
+
let(:destination) { '/destination' }
|
8
13
|
let(:vcs_adapter) { fake(:git_adapter) }
|
14
|
+
let(:digester) { fake(:digester, hexdigest: 'dummy') }
|
9
15
|
let(:changeset) { fake(:changeset, head: head_commit_sha, repository_url: repository_url) }
|
10
16
|
let(:repository_url) { 'git://github.com/pawelpacana/citrus-core.git' }
|
11
17
|
let(:head_commit_sha) { 'deadbeef' }
|
18
|
+
let(:cache_root) { Dir.mktmpdir('cache_root') }
|
19
|
+
let(:cache_dir) { File.join(cache_root, 'dummy') }
|
12
20
|
|
13
21
|
context '#fetch' do
|
14
|
-
include FakeFS::SpecHelpers
|
15
|
-
|
16
22
|
context 'with empty cache' do
|
17
|
-
let(:cache_root) { fake(:pathname) }
|
18
|
-
let(:cache_dir) { fake(:pathname) }
|
19
23
|
|
20
24
|
before do
|
21
|
-
stub(cache_root).join(any_args) { cache_dir }
|
22
|
-
stub(cache_dir).exist? { false }
|
23
25
|
subject.fetch(changeset, destination)
|
24
26
|
end
|
25
27
|
|
28
|
+
it 'should create cache dir' do
|
29
|
+
expect(File.exist?(cache_dir)).to be_true
|
30
|
+
end
|
31
|
+
|
26
32
|
it 'should clone repository to cache dir' do
|
27
33
|
expect(vcs_adapter).to have_received.clone_repository(repository_url, cache_dir)
|
28
34
|
end
|
29
35
|
|
30
36
|
it 'should clone updated cache to destination' do
|
37
|
+
mock(digester).hexdigest(repository_url) { cache_dir }
|
31
38
|
expect(vcs_adapter).to have_received.clone_repository(cache_dir, destination)
|
32
39
|
end
|
33
40
|
end
|
34
41
|
|
35
42
|
context 'with repository clone in cache' do
|
36
|
-
let(:cache_root) { fake(:pathname) }
|
37
|
-
let(:cache_dir) { fake(:pathname) }
|
38
|
-
|
39
43
|
before do
|
40
|
-
|
41
|
-
stub(cache_dir).exist? { true }
|
44
|
+
FileUtils.mkpath(File.join(cache_dir, 'some_file'))
|
42
45
|
subject.fetch(changeset, destination)
|
43
46
|
end
|
44
47
|
|
@@ -52,12 +55,10 @@ describe Citrus::Core::CachedCodeFetcher do
|
|
52
55
|
end
|
53
56
|
|
54
57
|
context do
|
55
|
-
let(:cache_root) { Pathname.new('/cache_root') }
|
56
|
-
|
57
58
|
before { subject.fetch(changeset, destination) }
|
58
59
|
|
59
60
|
it 'should create cache dir for repository' do
|
60
|
-
expect(cache_root.children).to have(1).element
|
61
|
+
expect(Pathname.new(cache_root).children).to have(1).element
|
61
62
|
end
|
62
63
|
|
63
64
|
it 'should checkout head commit from changeset' do
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Ruby syntax' do
|
4
|
+
|
5
|
+
it 'is clean' do
|
6
|
+
expect(citrus_core_warnings).to eq([])
|
7
|
+
end
|
8
|
+
|
9
|
+
def citrus_core_warnings
|
10
|
+
warnings.select { |w| w =~ %r{lib/citrus/core} }
|
11
|
+
end
|
12
|
+
|
13
|
+
def warnings
|
14
|
+
`ruby -Ilib -w lib/citrus/core.rb 2>&1`.split("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
File without changes
|
File without changes
|
@@ -5,7 +5,7 @@ describe Citrus::Core::ConfigurationLoader do
|
|
5
5
|
subject { described_class.new(validator) }
|
6
6
|
|
7
7
|
let(:validator) { fake(:configuration_validator) }
|
8
|
-
let(:test_root) { Pathname.new(File.dirname(__FILE__)) }
|
8
|
+
let(:test_root) { Pathname.new(File.dirname(__FILE__)).join('../../') }
|
9
9
|
let(:repo_root) { test_root.join('fixtures/repo') }
|
10
10
|
|
11
11
|
context '#load_from_path' do
|
File without changes
|
File without changes
|
@@ -2,56 +2,61 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
class Subscriber
|
4
4
|
def build_started(build); end
|
5
|
-
def build_failed(build,
|
6
|
-
def build_succeeded(build,
|
5
|
+
def build_failed(build, result); end
|
6
|
+
def build_succeeded(build, result); end
|
7
7
|
def build_aborted(build, reason); end
|
8
8
|
end
|
9
9
|
|
10
|
-
describe Citrus::Core::
|
10
|
+
describe Citrus::Core::ExecuteBuild do
|
11
11
|
|
12
12
|
subject { described_class.new(workspace_builder, configuration_loader, test_runner) }
|
13
13
|
|
14
14
|
let(:workspace_builder) { fake(:workspace_builder, create_workspace: path) }
|
15
|
-
let(:test_runner) { fake(:test_runner, start:
|
16
|
-
let(:build) { fake(:build) }
|
15
|
+
let(:test_runner) { fake(:test_runner, start: exit_code) }
|
16
|
+
let(:build) { fake(:build, output: test_output) }
|
17
17
|
let(:configuration_loader) { fake(:configuration_loader, load_from_path: configuration) }
|
18
18
|
let(:configuration) { fake(:configuration) }
|
19
19
|
let(:path) { fake }
|
20
20
|
let(:subscriber) { fake(:subscriber) }
|
21
|
-
let(:
|
22
|
-
let(:
|
23
|
-
let(:result_output) { StringIO.new }
|
21
|
+
let(:exit_code) { fake(:exit_code) }
|
22
|
+
let(:test_output) { fake(:test_output) }
|
24
23
|
|
25
24
|
context '#start' do
|
26
25
|
before { subject.add_subscriber(subscriber) }
|
27
26
|
|
28
27
|
context do
|
29
|
-
before { subject.start(build) }
|
30
|
-
|
31
28
|
it 'should prepare workspace for build' do
|
29
|
+
subject.start(build)
|
32
30
|
expect(workspace_builder).to have_received.create_workspace(build)
|
33
31
|
end
|
34
32
|
|
35
33
|
it 'should read build configuration from workspace' do
|
34
|
+
subject.start(build)
|
36
35
|
expect(configuration_loader).to have_received.load_from_path(path)
|
37
36
|
end
|
38
37
|
|
39
38
|
it 'should execute build script' do
|
40
|
-
|
39
|
+
subject.start(build)
|
40
|
+
expect(test_runner).to have_received.start(build, configuration, path)
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'should publish build_started event when starting build' do
|
44
|
+
subject.start(build)
|
44
45
|
expect(subscriber).to have_received.build_started(build)
|
45
46
|
end
|
46
47
|
|
47
48
|
it 'should publish build_succeeded event when build has succeeded' do
|
48
|
-
stub(
|
49
|
-
|
49
|
+
stub(exit_code).success? { true }
|
50
|
+
stub(test_runner).start(any_args) { exit_code }
|
51
|
+
subject.start(build)
|
52
|
+
expect(subscriber).to have_received.build_succeeded(build, exit_code)
|
50
53
|
end
|
51
54
|
|
52
55
|
it 'should publish build_failed event when build has failed' do
|
53
|
-
stub(
|
54
|
-
|
56
|
+
stub(exit_code).failure? { true }
|
57
|
+
stub(test_runner).start(any_args) { exit_code }
|
58
|
+
subject.start(build)
|
59
|
+
expect(subscriber).to have_received.build_failed(build, exit_code)
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
@@ -1,6 +1,12 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Citrus::Core::
|
3
|
+
describe Citrus::Core::ExitCode do
|
4
|
+
|
5
|
+
it 'should expose exit code value' do
|
6
|
+
exit_code = 0
|
7
|
+
subject = described_class.new(exit_code)
|
8
|
+
expect(subject.value).to eq(exit_code)
|
9
|
+
end
|
4
10
|
|
5
11
|
it 'should be successful for zero exit code value' do
|
6
12
|
subject = described_class.new(0)
|
@@ -12,9 +18,4 @@ describe Citrus::Core::TestResult do
|
|
12
18
|
expect(subject.failure?).to be_true
|
13
19
|
end
|
14
20
|
|
15
|
-
it 'should have IO like output' do
|
16
|
-
subject = described_class.new(0)
|
17
|
-
expect(subject.output).to respond_to(:rewind)
|
18
|
-
end
|
19
|
-
|
20
21
|
end
|
@@ -4,7 +4,7 @@ describe Citrus::Core::GithubAdapter do
|
|
4
4
|
|
5
5
|
subject { described_class.new }
|
6
6
|
|
7
|
-
let(:push_data) { Pathname.new(File.dirname(__FILE__)).join('fixtures/github_push_data.json').read }
|
7
|
+
let(:push_data) { Pathname.new(File.dirname(__FILE__)).join('../../fixtures/github_push_data.json').read }
|
8
8
|
|
9
9
|
context '#create_changeset_from_push_data' do
|
10
10
|
let(:changeset) { subject.create_changeset_from_push_data(push_data) }
|
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Citrus::Core::TestOutput do
|
4
|
+
|
5
|
+
let(:test_output) { described_class.new }
|
6
|
+
|
7
|
+
specify { expect(test_output).to respond_to(:read) }
|
8
|
+
specify { expect(test_output).to respond_to(:write) }
|
9
|
+
|
10
|
+
it 'should accumulate test output' do
|
11
|
+
chunks = %w(kaka dudu)
|
12
|
+
chunks.each { |c| test_output.write(c) }
|
13
|
+
expect(test_output.read).to eq(chunks.join)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Subscriber
|
4
|
+
def build_output_received(build, data); end
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Citrus::Core::TestRunner do
|
8
|
+
|
9
|
+
subject { described_class.new }
|
10
|
+
|
11
|
+
let(:configuration) { fake(:configuration, build_script: 'hostname') }
|
12
|
+
let(:path) { Pathname.new('/') }
|
13
|
+
let(:process) { fake(io: process_io, exit_code: exit_code) { ChildProcess::AbstractProcess } }
|
14
|
+
let(:process_io) { fake { ChildProcess::AbstractIO } }
|
15
|
+
let(:exit_code) { fake(:fixnum) }
|
16
|
+
let(:subscriber) { fake(:subscriber) }
|
17
|
+
let(:build) { fake(:build, output: build_output) }
|
18
|
+
let(:build_output) { fake(:test_output) }
|
19
|
+
|
20
|
+
context '#start' do
|
21
|
+
context do
|
22
|
+
before { stub(ChildProcess).build(configuration.build_script) { process } }
|
23
|
+
|
24
|
+
it 'spawns child process' do
|
25
|
+
subject.start(build, configuration, path)
|
26
|
+
expect(process).to have_received.start
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should return test result' do
|
30
|
+
expect(subject.start(build, configuration, path)).to be_kind_of(Citrus::Core::ExitCode)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should wait for process to finish' do
|
34
|
+
subject.start(build, configuration, path)
|
35
|
+
expect(process).to have_received.wait
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should publish build_output_received event when process produced output' do
|
40
|
+
subject.add_subscriber(subscriber)
|
41
|
+
subject.start(build, configuration, path)
|
42
|
+
expect(subscriber).to have_received.build_output_received(build, `hostname`)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should write process output to build' do
|
46
|
+
subject.start(build, configuration, path)
|
47
|
+
expect(build_output).to have_received.write(`hostname`)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should allow adding subscribers' do
|
52
|
+
expect(subject).to respond_to(:add_subscriber)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
describe Citrus::Core::WorkspaceBuilder do
|
5
|
+
|
6
|
+
subject { described_class.new(build_root, code_fetcher) }
|
7
|
+
|
8
|
+
let(:build) { fake(:build, uuid: uuid, changeset: changeset) }
|
9
|
+
let(:uuid) { SecureRandom.uuid }
|
10
|
+
let(:changeset) { fake(:changeset) }
|
11
|
+
let(:code_fetcher) { fake(:cached_code_fetcher) }
|
12
|
+
let(:build_root) { Dir.mktmpdir('build_root') }
|
13
|
+
|
14
|
+
context '#create_workspace' do
|
15
|
+
let(:result) { subject.create_workspace(build) }
|
16
|
+
|
17
|
+
it 'should create build directory under build root' do
|
18
|
+
expect(result).to start_with(build_root)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should create workspace in directories partitioned by date' do
|
22
|
+
expect(File.dirname(result)).to match(%r{/\d{4}/\d{2}/\d{2}\Z})
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should create workspace named after builds uuid' do
|
26
|
+
expect(File.basename(result)).to eql(uuid)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should fetch code needed for build into workspace' do
|
30
|
+
stub(Time).now { Time.at(0) }
|
31
|
+
result
|
32
|
+
expect(code_fetcher).to have_received.fetch(changeset, "#{build_root}/1970/01/01/#{uuid}")
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|