local_ci 0.0.3 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e82b2aed28cefc2d6c08727b6ea92821c2a8798786242c51eca088ad3b333fa
4
- data.tar.gz: 1ee3d798f7756d4f9a6c4667a6d495a181665ab952b97de3a667dd2251f40e43
3
+ metadata.gz: 408a3e34c26196b5f280ffb77aa68162ea4118a13309d29365bb4e064ccc940f
4
+ data.tar.gz: 893f1167a5a294e58f9f8d27452c2e85879263cbc4e9cbe934ef555a760eb257
5
5
  SHA512:
6
- metadata.gz: c246d0a0dc55146c54d945070c5c9b14cfc888e3ff581c6755fbafcd2569bcb820e286e76a9d3e5604512bc56752c3ba7cc947725340b9219a1c9329e43328bc
7
- data.tar.gz: 23d2bc6e09c66226e7fa7e6bc7673e2f7ea7e047c6903822abe94a5de80aad4e948252eb3275a797b721bb03fffe735efe8991cb433f0a2685ef873a7829c25e
6
+ metadata.gz: d5e5b78327fa679adf3b29a7b658a7134713d07dd20abb19dde41130d373b490bb00c1241987886beafaa8dbb4c47abf64b0ab07a513f412dcbb3495f09eaef4
7
+ data.tar.gz: c36f886be0c3ddd7f98d24d850c89dabc2962cdf286baa8fde1fd927deab952ea5bc39b174609882e80992f8742546e62cb5f79b129b87ae3b85e399a7c48f5d
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- local_ci (0.0.2)
4
+ local_ci (0.0.4)
5
5
  logger
6
6
  pastel
7
7
  rake
8
8
  tty-command
9
- tty-spinner
9
+ tty-cursor
10
+ tty-screen
10
11
 
11
12
  GEM
12
13
  remote: https://rubygems.org/
@@ -78,8 +79,7 @@ GEM
78
79
  tty-command (0.10.1)
79
80
  pastel (~> 0.8)
80
81
  tty-cursor (0.7.1)
81
- tty-spinner (0.9.3)
82
- tty-cursor (~> 0.7)
82
+ tty-screen (0.8.2)
83
83
  unicode-display_width (3.2.0)
84
84
  unicode-emoji (~> 4.1)
85
85
  unicode-emoji (4.1.0)
data/Rakefile CHANGED
@@ -36,8 +36,6 @@ flow "Build" do
36
36
  job "Compile - #{file}" do
37
37
  run "echo", "gcc", file
38
38
  end
39
-
40
- job "Boom", "exit 1"
41
39
  end
42
40
 
43
41
  teardown do
@@ -1,18 +1,10 @@
1
1
  module LocalCI
2
2
  class Failure
3
- attr_reader :job
3
+ attr_reader :job, :message
4
4
 
5
5
  def initialize(job:, message:)
6
6
  @job = job
7
7
  @message = message
8
8
  end
9
-
10
- def message
11
- <<~STR
12
- #{LocalCI::Helper.pastel.bold.red("FAIL:")} #{@job}
13
- #{@message}
14
-
15
- STR
16
- end
17
9
  end
18
10
  end
data/lib/local_ci/flow.rb CHANGED
@@ -1,25 +1,17 @@
1
1
  module LocalCI
2
2
  class Flow
3
- attr_accessor :task, :heading, :spinner, :failures
3
+ attr_accessor :task, :heading, :spinner, :failures, :jobs, :output
4
4
 
5
5
  def initialize(name:, heading:, parallel:, block:, actions: true)
6
6
  @task = name
7
7
  @task = "ci:#{name}" unless @task.start_with?("ci:")
8
8
  @parallel = parallel
9
9
  @failures = []
10
- @heading = heading
11
10
  @actions = actions
12
- @spinner = TTY::Spinner::Multi.new(
13
- "[:spinner] #{LocalCI::Helper.pastel.bold.blue(@heading)}",
14
- format: :classic,
15
- success_mark: LocalCI::Helper.pastel.green("✓"),
16
- error_mark: LocalCI::Helper.pastel.red("✗"),
17
- style: {
18
- top: "",
19
- middle: " ",
20
- bottom: " "
21
- }
22
- )
11
+
12
+ @heading = heading
13
+ @jobs = []
14
+ @output = LocalCI::Output.new(flow: self)
23
15
 
24
16
  setup_required_tasks
25
17
  if actions?
@@ -59,14 +51,12 @@ module LocalCI
59
51
  LocalCI::Task.new("#{@task}:jobs", parallel_prerequisites: @parallel)
60
52
 
61
53
  LocalCI::Task["#{@task}:teardown"]
62
- LocalCI::Task["#{@task}:failure_check"]
63
54
  LocalCI::Task[@task, @heading]
64
55
 
65
56
  LocalCI::Task["#{@task}:setup"]
66
57
  LocalCI::Task["#{@task}:setup"].add_prerequisite "ci:setup"
67
58
 
68
59
  LocalCI::Task[@task].add_prerequisite "#{@task}:teardown"
69
- LocalCI::Task["#{@task}:failure_check"].add_prerequisite "#{@task}:teardown"
70
60
  LocalCI::Task["#{@task}:teardown"].add_prerequisite "#{@task}:jobs"
71
61
  LocalCI::Task["#{@task}:jobs"].add_prerequisite "#{@task}:setup"
72
62
 
@@ -75,10 +65,8 @@ module LocalCI
75
65
 
76
66
  def setup_actionless_flow_tasks
77
67
  LocalCI::Task.new("#{@task}:jobs", parallel_prerequisites: @parallel)
78
- LocalCI::Task["#{@task}:failure_check"]
79
68
 
80
- LocalCI::Task[@task].add_prerequisite "#{@task}:failure_check"
81
- LocalCI::Task["#{@task}:failure_check"].add_prerequisite "#{@task}:jobs"
69
+ LocalCI::Task[@task].add_prerequisite "#{@task}:jobs"
82
70
  end
83
71
 
84
72
  def after_jobs
@@ -88,11 +76,9 @@ module LocalCI
88
76
 
89
77
  if @failures.any?
90
78
  LocalCI::Task["ci:teardown"].invoke
91
- @failures.each do |failure|
92
- puts failure.message
93
- end
79
+ output.failures
94
80
 
95
- abort LocalCI::Helper.pastel.red("#{@heading} failed, see CI.log for more.")
81
+ abort LocalCI::Helper.pastel.red("#{@heading} failed, see ci.log for more.")
96
82
  end
97
83
  end
98
84
  end
@@ -1,7 +1,8 @@
1
1
  module LocalCI
2
2
  module Helper
3
- def self.pastel = @pastel ||= Pastel.new(enabled: TTY::Color.support?)
4
- def self.runner = @runner ||= TTY::Command.new(output: Logger.new("ci.log"))
3
+ def self.color? = TTY::Color.support?
4
+ def self.pastel = @pastel ||= Pastel.new(enabled: color?)
5
+ def self.runner = @runner ||= TTY::Command.new(color: color?, output: Logger.new("ci.log"))
5
6
 
6
7
  def self.taskize(heading)
7
8
  heading.downcase.gsub(/\s/, "_").gsub(/[^\w]/, "").to_sym
data/lib/local_ci/job.rb CHANGED
@@ -1,19 +1,23 @@
1
1
  module LocalCI
2
2
  class Job
3
- attr_accessor :flow, :name, :command, :block, :task, :spinner
3
+ attr_reader :flow, :name, :command, :block, :task, :state, :duration
4
4
 
5
5
  def initialize(flow:, name:, command:, block:)
6
6
  @flow = flow
7
- @name = name
7
+ @flow.jobs << self
8
+ @task = "#{@flow.task}:jobs:#{LocalCI::Helper.taskize(name)}"
9
+
8
10
  @command = command
9
11
  @block = block
10
- @task = "#{@flow.task}:jobs:#{LocalCI::Helper.taskize(name)}"
12
+
13
+ @name = name
14
+ @state = :waiting
11
15
 
12
16
  raise ArgumentError, "Must specify a block or command" unless command || block
13
- @spinner = flow.spinner.register("[:spinner] #{name}")
14
17
 
15
18
  ::Rake::Task.define_task(task) do
16
- @spinner.auto_spin
19
+ @state = :running
20
+ @flow.output.update(self)
17
21
  start = Time.now
18
22
 
19
23
  if block
@@ -21,20 +25,27 @@ module LocalCI
21
25
  else
22
26
  LocalCI::Helper.runner.run(*command)
23
27
  end
24
-
25
- took = Time.now - start
26
- @spinner.success("(#{took.round(2)}s)")
28
+ @state = :success
27
29
  rescue TTY::Command::ExitError => e
28
- spinner.error
30
+ @state = :failed
29
31
  @flow.failures << LocalCI::Failure.new(
30
32
  job: @name,
31
33
  message: e.message
32
34
  )
35
+ ensure
36
+ @duration = Time.now - start
37
+ @flow.output.update(self)
33
38
  end
34
39
 
35
40
  ::Rake::Task["#{@flow.task}:jobs"].prerequisites << task
36
41
 
37
42
  ::Rake::Task[task].prerequisites << "#{@flow.task}:setup" if @flow.actions?
38
43
  end
44
+
45
+ def waiting? = @state == :waiting
46
+ def running? = @state == :running
47
+ def success? = @state == :success
48
+ def failed? = @state == :failed
49
+ def done? = [:success, :failed].include? @state
39
50
  end
40
51
  end
@@ -0,0 +1,150 @@
1
+ module LocalCI
2
+ class Output
3
+ attr_reader :job
4
+
5
+ def initialize(flow:)
6
+ @flow = flow
7
+ @first_paint = true
8
+ @start = 0
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ def start_thread
13
+ @thread = Thread.new {
14
+ loop do
15
+ @mutex.synchronize { draw(final: @thread_should_exit) }
16
+ break if @thread_should_exit
17
+ sleep 0.1
18
+ end
19
+ }
20
+ end
21
+
22
+ def finish
23
+ @thread_should_exit = true
24
+ @thread.join
25
+ end
26
+
27
+ def pastel = LocalCI::Helper.pastel
28
+ def cursor = TTY::Cursor
29
+ def screen = TTY::Screen
30
+ def tty? = $stdout.isatty
31
+
32
+ def passed? = @flow.jobs.all?(&:success?)
33
+ def failed? = @flow.failures.any?
34
+ def done? = @flow.jobs.all?(&:done?)
35
+
36
+ def update(job)
37
+ if tty?
38
+ finish and return if done?
39
+
40
+ return if @thread&.alive?
41
+
42
+ start_thread
43
+ else
44
+ @job = job
45
+
46
+ @mutex.synchronize { json_output }
47
+ end
48
+ end
49
+
50
+ def failures
51
+ return unless tty?
52
+
53
+ @mutex.synchronize {
54
+ @flow.failures.each do |failure|
55
+ puts <<~STR
56
+ #{pastel.bold.red("FAIL:")} #{failure.job}
57
+ #{failure.message}
58
+
59
+ STR
60
+ end
61
+ }
62
+ end
63
+
64
+ def draw(final: false)
65
+ if @first_paint
66
+ @start = Time.now
67
+ @first_paint = false
68
+ else
69
+ print cursor.clear_line + cursor.up(@flow.jobs.size + 2)
70
+ end
71
+
72
+ puts heading_line
73
+ @flow.jobs.each { puts job_line it }
74
+ puts footer_line
75
+
76
+ puts if final
77
+ end
78
+
79
+ def color(message)
80
+ if passed?
81
+ pastel.green(message)
82
+ elsif failed?
83
+ pastel.red(message)
84
+ else
85
+ pastel.blue(message)
86
+ end
87
+ end
88
+
89
+ def heading_line
90
+ heading = @flow.heading.dup
91
+ heading = "#{heading[...(screen.width - 11)]}…" if heading.length > (screen.width - 10)
92
+
93
+ tail_length = screen.width - 5 - heading.length - 2
94
+
95
+ color "===| #{pastel.bold heading} |#{"=" * tail_length}"
96
+ end
97
+
98
+ def job_line(job)
99
+ name = job.name.dup
100
+
101
+ name = "#{name[...(screen.width - 4)]}…" if name.length > (screen.width - 3)
102
+
103
+ result = cursor.clear_line
104
+ if job.waiting?
105
+ result << "[ ] #{name}"
106
+ elsif job.running?
107
+ result << "[-] #{name}"
108
+ elsif job.success?
109
+ result << "[#{pastel.green "✓"}] #{name}"
110
+ elsif job.failed?
111
+ result << "[#{pastel.red "✗"}] #{name}"
112
+ end
113
+
114
+ result
115
+ end
116
+
117
+ def duration
118
+ seconds = Time.now - @start
119
+ minutes = seconds / 60
120
+ hours = minutes / 60
121
+
122
+ seconds %= 60
123
+ minutes %= 60
124
+
125
+ if hours >= 1
126
+ "#{hours}h #{minutes}m"
127
+
128
+ elsif minutes >= 1
129
+ "#{minutes}m #{seconds}s"
130
+
131
+ else
132
+ "#{seconds.round(2)}s"
133
+ end
134
+ end
135
+
136
+ def footer_line
137
+ start_length = screen.width - 4 - duration.size - 1
138
+ color "#{"-" * start_length}(#{pastel.bold duration})---"
139
+ end
140
+
141
+ def json_output
142
+ puts({
143
+ flow: @flow.heading,
144
+ job: @job.name,
145
+ duration: @job.duration,
146
+ state: @job.state
147
+ }.to_json)
148
+ end
149
+ end
150
+ end
data/lib/local_ci.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  require "forwardable"
2
+ require "json"
2
3
  require "logger"
4
+
3
5
  require "tty-command"
4
- require "tty-spinner"
6
+ require "tty-cursor"
7
+ require "tty-screen"
5
8
  require "pastel"
6
9
 
7
10
  require "local_ci/dsl"
@@ -10,5 +13,6 @@ require "local_ci/failure"
10
13
  require "local_ci/flow"
11
14
  require "local_ci/helper"
12
15
  require "local_ci/job"
16
+ require "local_ci/output"
13
17
  require "local_ci/rake"
14
18
  require "local_ci/task"
data/local_ci.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "local_ci"
3
- s.version = "0.0.3"
3
+ s.version = "0.0.4"
4
4
  s.summary = "Run CI locally"
5
5
  s.description = "A way to run CI locally but also able to run easily on your hosted CI"
6
6
  s.authors = ["Sean Earle"]
@@ -14,5 +14,6 @@ Gem::Specification.new do |s|
14
14
  s.add_dependency "pastel"
15
15
  s.add_dependency "rake"
16
16
  s.add_dependency "tty-command"
17
- s.add_dependency "tty-spinner"
17
+ s.add_dependency "tty-cursor"
18
+ s.add_dependency "tty-screen"
18
19
  end
@@ -1,24 +1,12 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe LocalCI::Failure do
4
- describe "#message" do
5
- it "returns information about the failure" do
6
- pastel = double(:pastel)
7
- expect(LocalCI::Helper).to receive(:pastel).and_return(pastel)
8
- expect(pastel).to receive(:bold).and_return(pastel)
9
- expect(pastel).to receive(:red).with("FAIL:")
10
- .and_return("red")
4
+ describe "#initialize" do
5
+ it "sets up the instance variables" do
6
+ failure = LocalCI::Failure.new(job: "job", message: "message")
11
7
 
12
- failure = LocalCI::Failure.new(
13
- job: "job",
14
- message: "message"
15
- )
16
-
17
- expect(failure.message).to eq(<<~STR)
18
- red job
19
- message
20
-
21
- STR
8
+ expect(failure.job).to eq("job")
9
+ expect(failure.message).to eq("message")
22
10
  end
23
11
  end
24
12
  end
@@ -187,10 +187,14 @@ describe LocalCI::Flow do
187
187
 
188
188
  context "when there are failures" do
189
189
  before do
190
- @failure = double(:failure, message: "hi")
190
+ @failure = double(:failure, job: "job", message: "hi")
191
191
  @flow.failures << @failure
192
192
 
193
193
  allow(@flow).to receive(:abort)
194
+
195
+ @output = double(:output)
196
+ allow(@output).to receive(:failures)
197
+ allow(@flow).to receive(:output).and_return(@output)
194
198
  end
195
199
 
196
200
  it "runs ci:flow:teardown" do
@@ -206,13 +210,13 @@ describe LocalCI::Flow do
206
210
  end
207
211
 
208
212
  it "displays the failures" do
209
- expect(@failure).to receive(:message)
213
+ expect(@output).to receive(:failures)
210
214
 
211
215
  ::Rake::Task["ci:flow"].invoke
212
216
  end
213
217
 
214
218
  it "aborts with a message" do
215
- expect(@flow).to receive(:abort).with(/heading failed, see CI\.log for more\./)
219
+ expect(@flow).to receive(:abort).with(/heading failed, see ci\.log for more\./)
216
220
 
217
221
  ::Rake::Task["ci:flow"].invoke
218
222
  end
@@ -13,8 +13,10 @@ describe LocalCI::Helper do
13
13
 
14
14
  describe ".runner" do
15
15
  it "returns a TTY::Command instance" do
16
+ expect(TTY::Color).to receive(:support?).and_return("support")
16
17
  expect(Logger).to receive(:new).with("ci.log").and_return("logger")
17
- expect(TTY::Command).to receive(:new).with(output: "logger")
18
+ expect(TTY::Command).to receive(:new)
19
+ .with(color: "support", output: "logger")
18
20
  .and_return("tty-command")
19
21
 
20
22
  expect(LocalCI::Helper.runner).to eq("tty-command")
@@ -2,12 +2,16 @@ require "spec_helper"
2
2
 
3
3
  describe LocalCI::Job do
4
4
  before do
5
+ @output = double(:output)
6
+ allow(@output).to receive(:update)
7
+
5
8
  @flow = LocalCI::Flow.new(
6
9
  name: "flow",
7
10
  heading: "heading",
8
11
  parallel: true,
9
12
  block: -> {}
10
13
  )
14
+ allow(@flow).to receive(:output).and_return(@output)
11
15
  end
12
16
 
13
17
  describe "#initialize" do
@@ -24,6 +28,7 @@ describe LocalCI::Job do
24
28
  expect(job.command).to eq("command")
25
29
  expect(job.block).to eq("block")
26
30
  expect(job.task).to eq("ci:flow:jobs:the_job_name")
31
+ expect(job.state).to eq(:waiting)
27
32
  end
28
33
 
29
34
  context "when passed no block or command" do
@@ -80,6 +85,31 @@ describe LocalCI::Job do
80
85
 
81
86
  ::Rake::Task["ci:flow:jobs:block_task"].invoke
82
87
  end
88
+
89
+ it "sets the state" do
90
+ job = LocalCI::Job.new(
91
+ flow: @flow,
92
+ name: "block task",
93
+ command: nil,
94
+ block: -> {}
95
+ )
96
+
97
+ ::Rake::Task["ci:flow:jobs:block_task"].invoke
98
+
99
+ expect(job.state).to eq(:success)
100
+ end
101
+
102
+ it "updates the output" do
103
+ job = LocalCI::Job.new(
104
+ flow: @flow,
105
+ name: "block task",
106
+ command: nil,
107
+ block: -> {}
108
+ )
109
+ expect(@output).to receive(:update).with(job)
110
+
111
+ ::Rake::Task["ci:flow:jobs:block_task"].invoke
112
+ end
83
113
  end
84
114
 
85
115
  context "when running one command inline" do
@@ -97,6 +127,15 @@ describe LocalCI::Job do
97
127
  end
98
128
 
99
129
  context "when the job fails" do
130
+ before do
131
+ @job = LocalCI::Job.new(
132
+ flow: @flow,
133
+ name: "Raises an Error",
134
+ command: "exit 1",
135
+ block: nil
136
+ )
137
+ end
138
+
100
139
  it "is recorded against the flow" do
101
140
  expect(LocalCI::Helper.runner).to receive(:run).with("exit 1")
102
141
  .and_raise(
@@ -110,17 +149,23 @@ describe LocalCI::Job do
110
149
  )
111
150
  )
112
151
 
113
- LocalCI::Job.new(
114
- flow: @flow,
115
- name: "Raises an Error",
116
- command: "exit 1",
117
- block: nil
118
- )
119
152
  ::Rake::Task["ci:flow:jobs:raises_an_error"].invoke
120
153
 
121
154
  expect(@flow.failures.size).to eq(1)
122
155
  expect(@flow.failures.first.job).to eq("Raises an Error")
123
156
  end
157
+
158
+ it "sets the state" do
159
+ ::Rake::Task["ci:flow:jobs:raises_an_error"].invoke
160
+
161
+ expect(@job.state).to eq(:failed)
162
+ end
163
+
164
+ it "updates the output" do
165
+ expect(@output).to receive(:update).with(@job)
166
+
167
+ ::Rake::Task["ci:flow:jobs:raises_an_error"].invoke
168
+ end
124
169
  end
125
170
  end
126
171
  end
@@ -0,0 +1,406 @@
1
+ require "spec_helper"
2
+
3
+ describe LocalCI::Output do
4
+ before do
5
+ @job = double(:job)
6
+ @flow = double(:flow, jobs: [])
7
+ @thread = double(:thread, alive?: true)
8
+ allow(@thread).to receive(:join)
9
+
10
+ @output = LocalCI::Output.new(flow: @flow)
11
+ @output.instance_variable_set(:@thread, @thread)
12
+ end
13
+
14
+ describe "#update" do
15
+ before do
16
+ allow(@output).to receive(:output)
17
+ allow(@output).to receive(:json_output)
18
+ end
19
+
20
+ context "when we are not in a TTY" do
21
+ before do
22
+ expect(@output).to receive(:tty?).and_return(false)
23
+ end
24
+
25
+ it "sets the job" do
26
+ @output.update(@job)
27
+ expect(@output.job).to eq(@job)
28
+ end
29
+
30
+ it "calls json_output" do
31
+ expect(@output).to receive(:json_output)
32
+ @output.update(@job)
33
+ end
34
+ end
35
+
36
+ context "when we are in a TTY" do
37
+ before do
38
+ allow(@output).to receive(:tty?).and_return(true)
39
+ end
40
+
41
+ context "when all jobs are done" do
42
+ before do
43
+ allow(@output).to receive(:done?).and_return(true)
44
+ end
45
+
46
+ it "calls finish" do
47
+ expect(@output).to receive(:finish)
48
+ @output.update(@job)
49
+ end
50
+
51
+ it "does not call start_thread" do
52
+ expect(@output).not_to receive(:start_thread)
53
+ @output.update(@job)
54
+ end
55
+ end
56
+
57
+ context "the thread is alive" do
58
+ before do
59
+ allow(@output).to receive(:done?).and_return(false)
60
+ end
61
+
62
+ it "does not call finish" do
63
+ expect(@output).not_to receive(:finish)
64
+ @output.update(@job)
65
+ end
66
+
67
+ it "does not call start_thread" do
68
+ expect(@output).not_to receive(:start_thread)
69
+ @output.update(@job)
70
+ end
71
+ end
72
+
73
+ context "the thread is not alive" do
74
+ before do
75
+ allow(@output).to receive(:done?).and_return(false)
76
+ allow(@thread).to receive(:alive?).and_return(false)
77
+ end
78
+
79
+ it "starts the thread" do
80
+ expect(@output).to receive(:start_thread)
81
+
82
+ @output.update(@job)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ describe "#failures" do
89
+ before do
90
+ allow(@flow).to receive(:failures).and_return(
91
+ [
92
+ double(:failure, job: "job-1", message: "message-1"),
93
+ double(:failure, job: "job-2", message: "message-2")
94
+ ]
95
+ )
96
+ end
97
+
98
+ context "when we are in a TTY" do
99
+ it "displays the errors" do
100
+ expect(@output).to receive(:tty?).and_return(true)
101
+
102
+ expect(@output).to receive(:puts).with(/job-1\nmessage-1/)
103
+ expect(@output).to receive(:puts).with(/job-2\nmessage-2/)
104
+
105
+ @output.failures
106
+ end
107
+ end
108
+
109
+ context "when we are not in a TTY" do
110
+ it "does nothing" do
111
+ expect(@output).to receive(:tty?).and_return(false)
112
+
113
+ expect(@output).not_to receive(:puts)
114
+
115
+ @output.failures
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "#draw" do
121
+ before do
122
+ allow(@output).to receive(:print)
123
+ allow(@output).to receive(:puts)
124
+ allow(@output).to receive(:heading_line).and_return("heading-line")
125
+ allow(@output).to receive(:job_line).and_return("job-line")
126
+ allow(@output).to receive(:footer_line).and_return("footer-line")
127
+
128
+ allow(@flow).to receive(:jobs).and_return(["job-1", "job-2"])
129
+ end
130
+
131
+ context "on first paint for the flow" do
132
+ before do
133
+ @output.instance_variable_set(:@first_paint, true)
134
+ end
135
+
136
+ it "does not clear lines" do
137
+ expect(TTY::Cursor).not_to receive(:clear_line)
138
+ expect(TTY::Cursor).not_to receive(:up)
139
+
140
+ @output.draw
141
+ end
142
+
143
+ it "sets the start time" do
144
+ expect(Time).to receive(:now).and_return("time")
145
+
146
+ @output.draw
147
+
148
+ expect(@output.instance_variable_get(:@start)).to eq("time")
149
+ end
150
+ end
151
+
152
+ context "on subsequent paints" do
153
+ before do
154
+ @output.instance_variable_set(:@first_paint, false)
155
+
156
+ allow(TTY::Cursor).to receive(:clear_line).and_return("")
157
+ allow(TTY::Cursor).to receive(:up).and_return("")
158
+ allow(@output).to receive(:print)
159
+ end
160
+
161
+ it "moves up and clears the line" do
162
+ expect(TTY::Cursor).to receive(:clear_line).and_return("clear-line")
163
+ expect(TTY::Cursor).to receive(:up).with(4).and_return("up")
164
+
165
+ expect(@output).to receive(:print).with("clear-lineup")
166
+
167
+ @output.draw
168
+ end
169
+ end
170
+
171
+ it "paints the heading" do
172
+ expect(@output).to receive(:heading_line).and_return("heading-line")
173
+
174
+ expect(@output).to receive(:puts).with("heading-line")
175
+
176
+ @output.draw
177
+ end
178
+
179
+ it "paints the jobs" do
180
+ expect(@output).to receive(:job_line).with("job-1").and_return("job-line-1")
181
+ expect(@output).to receive(:job_line).with("job-2").and_return("job-line-2")
182
+
183
+ expect(@output).to receive(:puts).with("job-line-1")
184
+ expect(@output).to receive(:puts).with("job-line-2")
185
+
186
+ @output.draw
187
+ end
188
+
189
+ it "paints the footer" do
190
+ expect(@output).to receive(:footer_line).and_return("footer-line")
191
+
192
+ expect(@output).to receive(:puts).with("footer-line")
193
+
194
+ @output.draw
195
+ end
196
+
197
+ context "when final is true" do
198
+ it "draws an extra new line" do
199
+ expect(@output).to receive(:puts).with(no_args)
200
+
201
+ @output.draw(final: true)
202
+ end
203
+ end
204
+ end
205
+
206
+ describe "#color" do
207
+ before do
208
+ @pastel = double(:pastel)
209
+ allow(@output).to receive(:pastel).and_return(@pastel)
210
+ end
211
+
212
+ context "when the flow is running" do
213
+ it "returns blue" do
214
+ expect(@output).to receive(:passed?).and_return(false)
215
+ expect(@output).to receive(:failed?).and_return(false)
216
+
217
+ expect(@pastel).to receive(:blue).with("message")
218
+
219
+ @output.color("message")
220
+ end
221
+ end
222
+
223
+ context "when the flow has passed" do
224
+ it "returns blue" do
225
+ expect(@output).to receive(:passed?).and_return(true)
226
+
227
+ expect(@pastel).to receive(:green).with("message")
228
+
229
+ @output.color("message")
230
+ end
231
+ end
232
+
233
+ context "when the flow has failed" do
234
+ it "returns blue" do
235
+ expect(@output).to receive(:passed?).and_return(false)
236
+ expect(@output).to receive(:failed?).and_return(true)
237
+
238
+ expect(@pastel).to receive(:red).with("message")
239
+
240
+ @output.color("message")
241
+ end
242
+ end
243
+ end
244
+
245
+ describe "#heading_line" do
246
+ before do
247
+ allow(@flow).to receive(:heading).and_return("flow-heading")
248
+ allow(@output).to receive(:screen).and_return(double(width: 40))
249
+ @pastel = Pastel.new
250
+ end
251
+
252
+ it "returns a heading the width of the terminal" do
253
+ expect(@pastel.strip(@output.heading_line)).to eq(
254
+ "===| flow-heading |====================="
255
+ )
256
+ end
257
+
258
+ context "when the heading is too long" do
259
+ it "truncates it with an ellipsis" do
260
+ allow(@flow).to receive(:heading).and_return("Some really long heading that really probably shouldn't be this long but is I guess")
261
+ expect(@pastel.strip(@output.heading_line)).to eq(
262
+ "===| Some really long heading that… |==="
263
+ )
264
+ end
265
+ end
266
+
267
+ context "when the heading is just short enough" do
268
+ it "does not truncate" do
269
+ allow(@flow).to receive(:heading).and_return("Something long, but not silly!")
270
+
271
+ expect(@pastel.strip(@output.heading_line)).to eq(
272
+ "===| Something long, but not silly! |==="
273
+ )
274
+ end
275
+ end
276
+ end
277
+
278
+ describe "#job_line" do
279
+ before do
280
+ @pastel = Pastel.new
281
+
282
+ @job = double(:job, name: "job-name")
283
+ allow(@job).to receive(:waiting?).and_return(false)
284
+ allow(@job).to receive(:running?).and_return(false)
285
+ allow(@job).to receive(:success?).and_return(false)
286
+ allow(@job).to receive(:failed?).and_return(false)
287
+
288
+ allow(@output).to receive_message_chain(:cursor, :clear_line).and_return("")
289
+ allow(@output).to receive(:screen).and_return(double(width: 20))
290
+ end
291
+
292
+ context "when the job is waiting" do
293
+ it "returns no indicator and the job name" do
294
+ expect(@job).to receive(:waiting?).and_return(true)
295
+ expect(@pastel.strip(@output.job_line(@job))).to eq("[ ] job-name")
296
+ end
297
+ end
298
+
299
+ context "when the job is running" do
300
+ it "returns an indicator and the job name" do
301
+ expect(@job).to receive(:running?).and_return(true)
302
+ expect(@pastel.strip(@output.job_line(@job))).to eq("[-] job-name")
303
+ end
304
+ end
305
+
306
+ context "when the job was successful" do
307
+ it "returns an indicator and the job name" do
308
+ expect(@job).to receive(:success?).and_return(true)
309
+ expect(@pastel.strip(@output.job_line(@job))).to eq("[✓] job-name")
310
+ end
311
+ end
312
+
313
+ context "when the job failed" do
314
+ it "returns an indicator and the job name" do
315
+ expect(@job).to receive(:failed?).and_return(true)
316
+ expect(@pastel.strip(@output.job_line(@job))).to eq("[✗] job-name")
317
+ end
318
+ end
319
+
320
+ context "when the name is too long" do
321
+ it "truncates it with an ellipsis" do
322
+ allow(@job).to receive(:waiting?).and_return(true)
323
+ allow(@job).to receive(:name).and_return("A really long job name, obviously just way too long, what are you even doing!")
324
+
325
+ expect(@pastel.strip(@output.job_line(@job))).to eq(
326
+ "[ ] A really long jo…"
327
+ )
328
+ end
329
+ end
330
+
331
+ context "when the name is just short enough" do
332
+ it "does not truncate" do
333
+ allow(@job).to receive(:waiting?).and_return(true)
334
+ allow(@job).to receive(:name).and_return("A long job name!!")
335
+
336
+ expect(@pastel.strip(@output.job_line(@job))).to eq(
337
+ "[ ] A long job name!!"
338
+ )
339
+ end
340
+ end
341
+ end
342
+
343
+ describe "#footer_line" do
344
+ before do
345
+ allow(@output).to receive(:screen).and_return(double(width: 20))
346
+ allow(@output).to receive(:duration).and_return("duration")
347
+ @pastel = Pastel.new
348
+ end
349
+
350
+ it "returns a footer the length of the terminal with the duration" do
351
+ expect(@pastel.strip(@output.footer_line)).to eq(
352
+ "-------(duration)---"
353
+ )
354
+ end
355
+ end
356
+
357
+ describe "#duration" do
358
+ context "when less than 60 seconds" do
359
+ it "shows seconds with two decimal places" do
360
+ @output.instance_variable_set(:@start, 5.567)
361
+ allow(Time).to receive(:now).and_return(45)
362
+
363
+ expect(@output.duration).to eq("39.43s")
364
+ end
365
+ end
366
+
367
+ context "when less than 60 minutes" do
368
+ it "shows the minutes and seconds" do
369
+ @output.instance_variable_set(:@start, 10)
370
+ allow(Time).to receive(:now).and_return(1240)
371
+
372
+ expect(@output.duration).to eq("20m 30s")
373
+ end
374
+ end
375
+
376
+ context "when greater than 60 minutes" do
377
+ it "shows the hours and minutes" do
378
+ @output.instance_variable_set(:@start, 10)
379
+ allow(Time).to receive(:now).and_return(3800)
380
+
381
+ expect(@output.duration).to eq("1h 3m")
382
+ end
383
+ end
384
+ end
385
+
386
+ describe "#json_output" do
387
+ it "returns the state as json" do
388
+ allow(@flow).to receive(:heading).and_return("flow-heading")
389
+ @output.instance_variable_set(:@job, double(
390
+ :job,
391
+ name: "job-name",
392
+ duration: "job-duration",
393
+ state: "job-state"
394
+ ))
395
+
396
+ expect(@output).to receive(:puts).with({
397
+ flow: "flow-heading",
398
+ job: "job-name",
399
+ duration: "job-duration",
400
+ state: "job-state"
401
+ }.to_json)
402
+
403
+ @output.json_output
404
+ end
405
+ end
406
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: local_ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Earle
@@ -66,7 +66,21 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: tty-spinner
69
+ name: tty-cursor
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: tty-screen
70
84
  requirement: !ruby/object:Gem::Requirement
71
85
  requirements:
72
86
  - - ">="
@@ -96,6 +110,7 @@ files:
96
110
  - lib/local_ci/flow.rb
97
111
  - lib/local_ci/helper.rb
98
112
  - lib/local_ci/job.rb
113
+ - lib/local_ci/output.rb
99
114
  - lib/local_ci/rake.rb
100
115
  - lib/local_ci/task.rb
101
116
  - local_ci.gemspec
@@ -107,6 +122,7 @@ files:
107
122
  - spec/unit/local_ci/flow_spec.rb
108
123
  - spec/unit/local_ci/helper_spec.rb
109
124
  - spec/unit/local_ci/job_spec.rb
125
+ - spec/unit/local_ci/output_spec.rb
110
126
  - spec/unit/local_ci/rake_spec.rb
111
127
  - spec/unit/local_ci/task_spec.rb
112
128
  homepage: https://github.com/HellRok/local_ci