houston-core 0.5.4 → 0.5.5

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +20 -22
  3. data/README.md +1 -1
  4. data/app/adapters/houston/adapters/version_control/git_adapter/repo.rb +6 -3
  5. data/app/assets/javascripts/app/boot.coffee +9 -0
  6. data/app/assets/javascripts/app/infinite_scroll.coffee +6 -3
  7. data/app/assets/javascripts/app/models/ticket.coffee +1 -1
  8. data/app/assets/javascripts/core/app.coffee +4 -1
  9. data/app/assets/javascripts/core/core_ext/array.coffee +11 -0
  10. data/app/assets/javascripts/core/core_ext/date.coffee +8 -0
  11. data/app/assets/javascripts/core/handlebars_helpers.coffee +12 -8
  12. data/app/assets/javascripts/vendor.js +2 -2
  13. data/app/assets/stylesheets/application/mobile.scss +96 -0
  14. data/app/assets/stylesheets/application/test.scss +58 -0
  15. data/app/assets/stylesheets/application/test_run.scss +14 -5
  16. data/app/assets/stylesheets/application/timeline.scss +2 -4
  17. data/app/concerns/commit_synchronizer.rb +38 -2
  18. data/app/controllers/application_controller.rb +3 -0
  19. data/app/controllers/hooks_controller.rb +18 -0
  20. data/app/controllers/project_tests_controller.rb +46 -0
  21. data/app/helpers/commit_helper.rb +7 -0
  22. data/app/helpers/test_run_helper.rb +16 -0
  23. data/app/models/commit.rb +4 -0
  24. data/app/models/github/pull_request.rb +7 -7
  25. data/app/models/milestone.rb +1 -1
  26. data/app/models/run_tests_on_post_receive.rb +2 -0
  27. data/app/models/test.rb +4 -0
  28. data/app/models/test_result.rb +1 -1
  29. data/app/models/test_run.rb +25 -2
  30. data/app/views/layouts/_mobile_navigation.html.erb +100 -0
  31. data/app/views/layouts/application.html.erb +20 -10
  32. data/app/views/layouts/dashboard.html.erb +1 -1
  33. data/app/views/layouts/minimal.html.erb +1 -1
  34. data/app/views/layouts/naked_dashboard.html.erb +1 -1
  35. data/app/views/project_notification/test_run.html.erb +97 -120
  36. data/app/views/project_tests/_commits.html.erb +14 -0
  37. data/app/views/project_tests/index.html.erb +39 -0
  38. data/app/views/projects/_form.html.erb +6 -2
  39. data/config/application.rb +1 -2
  40. data/config/routes.rb +2 -0
  41. data/db/migrate/20151108221505_convert_pull_request_labels_to_array.rb +22 -0
  42. data/db/migrate/20151108223154_sync_body_also_for_pull_requests.rb +5 -0
  43. data/db/migrate/20151108233510_add_props_to_pull_requests.rb +5 -0
  44. data/db/structure.sql +10 -1
  45. data/houston.gemspec +4 -5
  46. data/lib/houston/version.rb +1 -1
  47. data/test/integration/web_hook_test.rb +7 -1
  48. data/test/unit/concerns/commit_synchronizer_test.rb +13 -0
  49. data/test/unit/models/pull_request_test.rb +17 -0
  50. data/vendor/assets/javascripts/showdown.js +2489 -0
  51. data/vendor/assets/javascripts/slideout.js +493 -0
  52. metadata +25 -29
  53. data/lib/tasks/config.rake +0 -255
  54. data/vendor/assets/javascripts/Markdown.Converter.js +0 -1412
@@ -44,6 +44,7 @@ h2.test-result-banner {
44
44
  &.pass { @include badge-style(#5DB64C); letter-spacing: 0.088em; } // grass
45
45
  &.error { @include badge-style(#DDC522); letter-spacing: 0.088em; } // mustard
46
46
  &.aborted { @include badge-style(#888888); letter-spacing: 0.088em; } // gray
47
+ &.pending { @include badge-style(#888888); letter-spacing: 0.088em; } // gray
47
48
  &.fail { @include badge-style(#E24E32); letter-spacing: 0.160em; } // tomato
48
49
 
49
50
  a {
@@ -75,12 +76,20 @@ h2.test-result-banner {
75
76
  .test {
76
77
  display: block;
77
78
  margin: 0;
78
- padding: 0.18em 0 0.18em 5.25em;
79
- text-indent: -4em;
80
79
 
81
- &.fail { background-color: rgba(226, 78, 50, 0.15); }
82
- &.error { background-color: rgb(252, 247, 207); }
83
- &.skip { }
80
+ & > a {
81
+ display: block;
82
+ padding: 0.18em 0 0.18em 5.25em;
83
+ text-indent: -4em;
84
+
85
+ color: inherit;
86
+ text-decoration: none !important;
87
+
88
+ &:hover { background-color: #edf9ff; }
89
+ }
90
+
91
+ &.fail > a { background-color: rgba(226, 78, 50, 0.15); }
92
+ &.fail > a:hover { background-color: rgba(226, 78, 50, 0.33); }
84
93
  }
85
94
 
86
95
  .test-suite-name {
@@ -39,11 +39,11 @@ $lightGray: #eee;
39
39
 
40
40
  .timeline-event {
41
41
  margin-left: 50px;
42
- padding: 4px 4px 4px 16px;
42
+ padding: 4px 4px 4px 70px;
43
43
  border-left: solid 2px $lightGray;
44
44
  position: relative;
45
45
  font-size: 0.92em;
46
- text-indent: -21px;
46
+ text-indent: -57px;
47
47
  line-height: 1.25em;
48
48
 
49
49
  .timeline-event-time {
@@ -131,8 +131,6 @@ $lightGray: #eee;
131
131
  &.timeline-event-ticket-created,
132
132
  &.timeline-event-ticket-closed {
133
133
  padding-right: 100px;
134
- text-indent: -57px;
135
- padding-left: 70px;
136
134
  }
137
135
 
138
136
  &.timeline-event-release {
@@ -8,7 +8,12 @@ module CommitSynchronizer
8
8
  expected_commits = repo.all_commits
9
9
 
10
10
  create_missing_commits! expected_commits - existing_commits
11
- flag_unreachable_commits! existing_commits - expected_commits
11
+
12
+ reachable_commits = project.commits.reachable.pluck(:sha)
13
+ flag_unreachable_commits! reachable_commits - expected_commits
14
+
15
+ unreachable_commits = project.commits.unreachable.pluck(:sha)
16
+ flag_reachable_commits! unreachable_commits & expected_commits
12
17
  end
13
18
  end
14
19
 
@@ -29,7 +34,13 @@ module CommitSynchronizer
29
34
  end
30
35
 
31
36
 
32
- def synchronize(native_commits)
37
+ def synchronize(native_commits=[])
38
+ if block_given?
39
+ native_commits = Houston.benchmark("[commits.synchonize] reading commits") do
40
+ yield repo
41
+ end
42
+ end
43
+
33
44
  native_commits = native_commits.reject(&:nil?)
34
45
  return [] if native_commits.empty?
35
46
 
@@ -100,6 +111,31 @@ private
100
111
  Houston.report_exception $!
101
112
  end
102
113
 
114
+ def flag_reachable_commits!(reachable_commits)
115
+ return if reachable_commits.none?
116
+
117
+ # Inserting this to help troubleshoot a scenario where PG::TRDeadlockDetected
118
+ # is raised from this method. This is a recoverable scenario, so we report
119
+ # the exception (with additional context) but do not re-raise it.
120
+ query = <<-SQL
121
+ SELECT query, state, waiting, pid
122
+ FROM pg_stat_activity
123
+ WHERE state <> 'idle' AND waiting='t'
124
+ SQL
125
+ waiting_queries = connection.select_all(query).to_hash
126
+ .reject { |result| result["query"] == query }
127
+
128
+ project.commits.where(sha: reachable_commits).update_all(unreachable: false)
129
+
130
+ Rails.logger.info "[commits:sync] #{reachable_commits.length} reachable commits for #{project.name}"
131
+
132
+ rescue exceptions_wrapping(PG::TRDeadlockDetected)
133
+ $!.additional_information["project"] = project.slug
134
+ $!.additional_information["reachable_commits"] = reachable_commits.join("\n")
135
+ $!.additional_information["waiting_queries"] = MultiJson.dump(waiting_queries)
136
+ Houston.report_exception $!
137
+ end
138
+
103
139
  def repo
104
140
  project.repo
105
141
  end
@@ -9,6 +9,9 @@ class ApplicationController < ActionController::Base
9
9
  after_filter :save_current_project
10
10
 
11
11
 
12
+ delegate :mobile?, to: :browser
13
+ helper_method :mobile?
14
+
12
15
 
13
16
  rescue_from CanCan::AccessDenied do |exception|
14
17
  if current_user
@@ -28,4 +28,22 @@ class HooksController < ApplicationController
28
28
  end
29
29
  end
30
30
 
31
+ def trigger
32
+ event = "hooks:#{params[:hook]}"
33
+ unless Houston.observer.observed?(event)
34
+ render text: "A hook with the slug '#{params[:hook]}' is not defined", status: 404
35
+ return
36
+ end
37
+
38
+ payload = params.except(:action, :controller).merge({
39
+ sender: {
40
+ ip: request.remote_ip,
41
+ agent: request.user_agent
42
+ }
43
+ })
44
+
45
+ Houston.observer.fire event, payload
46
+ head 200
47
+ end
48
+
31
49
  end
@@ -0,0 +1,46 @@
1
+ class ProjectTestsController < ApplicationController
2
+
3
+ def index
4
+ @project = Project.find_by_slug! params[:slug]
5
+ @test = @project.tests.find params[:id]
6
+ @totals = Hash[@test.test_results.group(:status).pluck(:status, "COUNT(*)")]
7
+
8
+ begin
9
+ head = params.fetch :at, @project.repo.branch("master")
10
+ stop_shas = @test.introduced_in_shas
11
+ @commits = Houston.benchmark("[project_tests#index] fetch commits") {
12
+ @project.repo.ancestors(head, including_self: true, limit: 100, hide: stop_shas) }
13
+
14
+ if @commits.any?
15
+ @runs = @project.test_runs.where(sha: @commits.map(&:sha))
16
+
17
+ @commits.each do |commit|
18
+ def commit.date
19
+ @date ||= committed_at.to_date
20
+ end
21
+ def commit.time
22
+ committed_at
23
+ end
24
+ end
25
+
26
+ @results = @test.test_results.where(test_run_id: @runs.map(&:id))
27
+ .joins(:test_run)
28
+ .select("test_runs.sha", :*)
29
+ .index_by { |result| result[:sha] }
30
+ @runs = @runs.index_by(&:sha)
31
+ end
32
+ rescue Houston::Adapters::VersionControl::CommitNotFound
33
+ @commits = []
34
+ @exception = $!
35
+ end
36
+
37
+ if request.xhr?
38
+ if @commits.any?
39
+ render partial: "project_tests/commits"
40
+ else
41
+ head 204
42
+ end
43
+ end
44
+ end
45
+
46
+ end
@@ -61,4 +61,11 @@ module CommitHelper
61
61
  end
62
62
  end
63
63
 
64
+ def commit_test_message(commit)
65
+ message = commit.message[/^.*$/]
66
+ return message unless @project
67
+ return message unless @project.repo.respond_to? :commit_url
68
+ link_to message, @project.repo.commit_url(commit), target: "_blank"
69
+ end
70
+
64
71
  end
@@ -33,4 +33,20 @@ module TestRunHelper
33
33
  end
34
34
  end
35
35
 
36
+ def commit_test_status(test_run, test_result)
37
+ status = test_result.status if test_result
38
+ status = "untested" if test_run.nil?
39
+ status = "pending" if test_run && test_run.pending?
40
+ status = "aborted" if test_run && test_run.aborted?
41
+ status ||= "unknown"
42
+
43
+ css = "project-test-status project-test-status-#{status}"
44
+
45
+ if test_run
46
+ link_to status, test_run_url(slug: test_run.project.slug, commit: test_run.sha), class: css
47
+ else
48
+ "<span class=\"#{css}\">#{status}</span>".html_safe
49
+ end
50
+ end
51
+
36
52
  end
data/app/models/commit.rb CHANGED
@@ -43,6 +43,10 @@ class Commit < ActiveRecord::Base
43
43
  where(unreachable: false)
44
44
  end
45
45
 
46
+ def unreachable
47
+ where(unreachable: true)
48
+ end
49
+
46
50
  def latest
47
51
  last
48
52
  end
@@ -17,8 +17,7 @@ module Github
17
17
  after_create { Houston.observer.fire "github:pull:opened", self }
18
18
  after_update { Houston.observer.fire "github:pull:updated", self, changes }
19
19
 
20
- validates :project_id, :title, :number, :repo, :url, :base_ref, :base_sha, :head_ref, :head_sha,
21
- presence: true
20
+ validates :project_id, :title, :number, :repo, :url, :base_ref, :base_sha, :head_ref, :head_sha, :username, presence: true
22
21
  validates :number, uniqueness: { scope: :project_id }
23
22
 
24
23
  class << self
@@ -92,16 +91,16 @@ module Github
92
91
  number: github_pr["number"])
93
92
  .merge_attributes(github_pr)
94
93
  end
95
- end
96
94
 
95
+ def labeled(*labels)
96
+ where(["labels && ARRAY[?]", labels])
97
+ end
98
+ end
97
99
 
98
100
 
99
- def labels
100
- super.split(/\n/)
101
- end
102
101
 
103
102
  def labels=(value)
104
- super Array(value).uniq.join("\n")
103
+ super Array(value).uniq
105
104
  end
106
105
 
107
106
  def add_label!(label, options={})
@@ -131,6 +130,7 @@ module Github
131
130
  end
132
131
 
133
132
  self.title = pr["title"]
133
+ self.body = pr["body"]
134
134
  self.head_sha = pr["head"]["sha"]
135
135
  self.head_ref = pr["head"]["ref"]
136
136
  self.labels = pr["labels"] if pr.key?("labels")
@@ -4,7 +4,7 @@ class Milestone < ActiveRecord::Base
4
4
  belongs_to :project
5
5
  has_many :tickets, -> { reorder("NULLIF(tickets.extended_attributes->'milestoneSequence', '')::int") }
6
6
 
7
- versioned only: [:name, :start_date, :end_date, :band, :lanes], class_name: "MilestoneVersion", initial_version: true
7
+ versioned only: [:name, :start_date, :end_date, :band, :lanes, :destroyed_at], class_name: "MilestoneVersion", initial_version: true
8
8
 
9
9
  default_scope { where(destroyed_at: nil).order(:start_date) }
10
10
 
@@ -148,6 +148,8 @@ class RunTestsOnPostReceive
148
148
  return if test_run.project.code_climate_repo_token.blank?
149
149
  CodeClimate::CoverageReport.publish!(test_run)
150
150
  test_run.project.feature_working! :publish_coverage_to_code_climate
151
+ rescue Houston::Adapters::VersionControl::CommitNotFound
152
+ # Got a bad Test Run, nothing we can do about it.
151
153
  rescue Net::OpenTimeout, Net::ReadTimeout
152
154
  test_run.project.feature_broken! :publish_coverage_to_code_climate
153
155
  Rails.logger.warn "\e[31m[push:publish:codeclimate] #{$!.class}: #{$!.message}\e[0m"
data/app/models/test.rb CHANGED
@@ -5,4 +5,8 @@ class Test < ActiveRecord::Base
5
5
 
6
6
  validates :project_id, :suite, :name, presence: true
7
7
 
8
+ def introduced_in_shas
9
+ test_results.where(new_test: true).joins(:test_run).pluck("test_runs.sha")
10
+ end
11
+
8
12
  end
@@ -11,7 +11,7 @@ class TestResult < ActiveRecord::Base
11
11
  return if attributes.none?
12
12
  columns = attributes.first.keys
13
13
  values = attributes.map(&:values)
14
- import columns, values
14
+ import columns, values, validate: false
15
15
  end
16
16
 
17
17
  end
@@ -63,6 +63,24 @@ class TestRun < ActiveRecord::Base
63
63
  AND test_runs.completed_at=most_recent_test_runs.completed_at
64
64
  SQL
65
65
  end
66
+
67
+ def rebuild_tests!(options={})
68
+ test_runs = where("tests is not null")
69
+ .where("id NOT IN (SELECT DISTINCT test_run_id FROM test_results)")
70
+ if options[:progress]
71
+ require "progressbar"
72
+ pbar = ProgressBar.new("test runs", test_runs.count)
73
+ end
74
+ test_runs.find_each do |test_run|
75
+ if test_run.read_attribute(:tests).nil?
76
+ test_run.update_column :tests, nil
77
+ else
78
+ test_run.save_tests_and_results
79
+ end
80
+ pbar.inc if options[:progress]
81
+ end
82
+ pbar.finish if options[:progress]
83
+ end
66
84
  end
67
85
 
68
86
 
@@ -181,6 +199,10 @@ class TestRun < ActiveRecord::Base
181
199
  completed_at.present?
182
200
  end
183
201
 
202
+ def pending?
203
+ !completed?
204
+ end
205
+
184
206
  def has_results?
185
207
  result.present? and !aborted?
186
208
  end
@@ -223,8 +245,9 @@ class TestRun < ActiveRecord::Base
223
245
  def tests
224
246
  @tests ||= test_results.includes(:error).joins(:test).select("test_results.*", "tests.suite", "tests.name").map do |test_result|
225
247
  message, backtrace = test_result.error.output.split("\n\n") if test_result.error
226
- { suite: test_result[:suite],
227
- name: test_result[:name],
248
+ { test_id: test_result.test_id,
249
+ suite: test_result[:suite],
250
+ name: test_result[:name].to_s.gsub(/^(test :|: )/, ""),
228
251
  status: test_result.status,
229
252
  duration: test_result.duration,
230
253
  error_message: message,
@@ -0,0 +1,100 @@
1
+ <div class="navbar navbar-fixed-top navbar-inverse">
2
+ <div class="navbar-inner">
3
+ <div class="container-fluid">
4
+ <%= link_to Houston.config.title, main_app.root_url, class: "brand" %>
5
+
6
+ <ul class="nav pull-right nav-inline">
7
+ <% if current_user -%>
8
+ <li class="current-user dropdown">
9
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown"><%= avatar_for(current_user, size: 30) %> <b class="caret"></b></a>
10
+ <ul class="dropdown-menu">
11
+ <li><%= link_to "Settings", main_app.edit_user_path(current_user) %></li>
12
+ <li><%= link_to "Sign out", main_app.destroy_user_session_path %></li>
13
+ </ul>
14
+ </li>
15
+ <% else -%>
16
+ <li><%= link_to "Sign in", main_app.new_user_session_path %></li>
17
+ <% end -%>
18
+ </ul>
19
+
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div id="slideout_menu" class="slideout-menu">
25
+ <div class="general-navbar">
26
+ <ul class="nav">
27
+ <% Houston.config.navigation.each do |navigation| %><%= render_navigation navigation %><% end %>
28
+
29
+ <li class="divider-horizontal"></li>
30
+
31
+ <% if can?(:read, Project) -%>
32
+ <%= render_nav_link "Projects", main_app.projects_path, icon: "fa-database" %>
33
+ <% end -%>
34
+
35
+ <% if can?(:read, User) -%>
36
+ <%= render_nav_link "Team", main_app.users_path, icon: "fa-user" %>
37
+ <% end -%>
38
+
39
+ <% if can?(:read, :job) -%>
40
+ <%= render_nav_link "Jobs", main_app.jobs_path, icon: "fa-user" %>
41
+ <% end -%>
42
+
43
+ </ul>
44
+ </div>
45
+
46
+ <% if current_project && current_project.persisted? %>
47
+ <div class="project-navbar <%= current_project.color %>">
48
+ <ul class="nav nav-inline">
49
+ <% if current_user -%>
50
+ <li class="dropdown current-project <%= current_project && current_project.color %>">
51
+ <a href="#" title="Feedback" class="dropdown-toggle" data-toggle="dropdown">
52
+ <%= current_project ? current_project.name : "Select Project" %>
53
+ </a>
54
+ <ul class="dropdown-menu">
55
+ <% followed_projects.each do |project| %>
56
+ <% if project == current_project %>
57
+ <li class="current">
58
+ <b class="bubble <%= project.color %>"></b> <%= project.name %></a>
59
+ </li>
60
+ <% else %>
61
+ <li>
62
+ <% path = if !current_feature
63
+ # we're not on a project page,
64
+ # just refresh the page and set the project
65
+ "?project=#{project.slug}"
66
+ elsif !project.features.include?(current_feature)
67
+ # we're using a feature that this project
68
+ # doesn't support. Navigate to the root URL
69
+ # and set the project
70
+ main_app.root_path(project: project.slug)
71
+ else
72
+ feature_path(project, current_feature)
73
+ end %>
74
+ <%= link_to path do %>
75
+ <b class="bubble <%= project.color %>"></b> <%= project.name %></a>
76
+ <% end %>
77
+ </li>
78
+ <% end %>
79
+ <% end %>
80
+ </ul>
81
+ </li>
82
+ <% end %>
83
+ </ul>
84
+ <% if current_project.features.any? %>
85
+ <ul class="nav">
86
+ <% current_project.features.each do |feature| %>
87
+ <%= render_nav_for_feature(feature) %>
88
+ <% end %>
89
+ </ul>
90
+ <% else %>
91
+ <div class="project-no-features">
92
+ No features are enabled for <%= current_project.name %>.
93
+ <% if can?(:update, current_project) %>
94
+ You can enable features in <%= link_to "Project Settings", main_app.edit_project_path(current_project) %>.
95
+ <% end %>
96
+ </div>
97
+ <% end %>
98
+ </div>
99
+ <% end %>
100
+ </div>