merging local with master

Merge remote-tracking branch 'upstream/master'
This commit is contained in:
Kris Aubuchon 2013-11-04 09:17:12 -05:00
commit 70fd4f7f6b
62 changed files with 1081 additions and 737 deletions

View file

@ -26,7 +26,9 @@ Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.P
}
var adminSettingsController = this;
return this.get('content').filter(function(item, index, enumerable) {
var maxResults = Em.isNone(filter) ? this.get('content.length') : 20;
return _.first(this.get('content').filter(function(item, index, enumerable) {
if (adminSettingsController.get('onlyOverridden') && !item.get('overridden')) return false;
if (filter) {
if (item.get('setting').toLowerCase().indexOf(filter) > -1) return true;
@ -36,20 +38,11 @@ Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.P
}
return true;
});
}), maxResults);
}.property('filter', 'content.@each', 'onlyOverridden'),
actions: {
/**
Changes the currently active filter
@method changeFilter
**/
changeFilter: function() {
this.set('filter', this.get('newFilter'));
},
/**
Reset a setting to its default value

View file

@ -6,8 +6,7 @@
</label>
</div>
<div class='controls'>
{{textField value=newFilter}}
<button class="btn btn-primary" {{action changeFilter}}>{{i18n filter}}</button>
{{textField value=filter placeholderKey="type_to_filter"}}
</div>
</div>

View file

@ -248,4 +248,3 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
});
Discourse.Router = Discourse.Router.reopen({ location: 'discourse_location' });

View file

@ -45,9 +45,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
cloned = range.cloneRange(),
$ancestor = $(range.commonAncestorContainer);
// don't display the "quote reply" button if you select text spanning two posts
// note: the ".contents" is here to prevent selection of the topic summary
if ($ancestor.closest('.topic-body > .contents').length === 0) {
if ($ancestor.closest('.cooked').length === 0) {
this.set('buffer', '');
return;
}

View file

@ -88,18 +88,18 @@ Discourse.Utilities = {
var html = '';
if (typeof window.getSelection !== "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection !== "undefined") {
if (document.selection.type === "Text") {
html = document.selection.createRange().htmlText;
}
if (document.selection.type === "Text") {
html = document.selection.createRange().htmlText;
}
}
// Strip out any .click elements from the HTML before converting it to text

View file

@ -32,6 +32,11 @@ Discourse.Post = Discourse.Model.extend({
notDeleted: Em.computed.not('deleted'),
userDeleted: Em.computed.empty('user_id'),
showName: function() {
var name = this.get('name');
return name && (name !== this.get('username')) && Discourse.SiteSettings.display_name_on_posts;
}.property('name', 'username'),
postDeletedBy: function() {
if (this.get('firstPost')) { return this.get('topic.deleted_by'); }
return this.get('deleted_by');

View file

@ -21,6 +21,11 @@
<div {{bindAttr class=":contents byTopicCreator:topic-creator :trigger-expansion"}}>
<a href='{{unbound usernameUrl}}' {{action showPosterExpansion this}}>{{avatar this imageSize="large"}}</a>
<h3 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}' {{action showPosterExpansion this}}>{{breakUp username}}</a></h3>
{{#if showName}}
<h3><a href='{{unbound usernameUrl}}' {{action showPosterExpansion this}}>{{breakUp name}}</a></h3>
{{/if}}
{{#if user_title}}<div class="user-title" {{action showPosterExpansion this}}>{{user_title}}</div>{{/if}}
</div>
{{else}}

View file

@ -64,8 +64,8 @@ Discourse.QuoteButtonView = Discourse.View.extend({
})
.on('selectionchange', function() {
// there is no need to handle this event when the mouse is down
// or if there is not a touch in progress
if (view.get('isMouseDown') || !view.get('isTouchInProgress')) return;
// or if there a touch in progress
if (view.get('isMouseDown') || view.get('isTouchInProgress')) return;
// `selection.anchorNode` is used as a target
view.selectText(window.getSelection().anchorNode, controller);
});

View file

@ -8,5 +8,3 @@ window.ENV = {
window.Discourse = {};
Discourse.SiteSettings = {};

View file

@ -60,8 +60,7 @@ I18n.locale = null;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
I18n.fallbackRules = {
};
I18n.fallbackRules = {};
I18n.pluralizationRules = {
en: function (n) {
@ -207,7 +206,7 @@ I18n.translate = function(scope, options) {
} else {
return this.interpolate(translation, options);
}
} catch(err) {
} catch (error) {
return this.missingTranslation(scope);
}
};
@ -485,15 +484,9 @@ I18n.findAndTranslateValidNode = function(keys, translation) {
I18n.pluralize = function(count, scope, options) {
var translation;
try {
translation = this.lookup(scope, options);
} catch (error) {}
try { translation = this.lookup(scope, options); } catch (error) {}
if (!translation) { return this.missingTranslation(scope); }
if (!translation) {
return this.missingTranslation(scope);
}
var message;
options = this.prepareOptions(options);
options.count = count.toString();
@ -501,24 +494,16 @@ I18n.pluralize = function(count, scope, options) {
var key = pluralizer(Math.abs(count));
var keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
message = this.findAndTranslateValidNode(keys, translation);
var message = this.findAndTranslateValidNode(keys, translation);
if (message == null) message = this.missingTranslation(scope, keys[0]);
return this.interpolate(message, options);
};
I18n.missingTranslation = function() {
var message = '[missing "' + this.currentLocale()
, count = arguments.length
;
for (var i = 0; i < count; i++) {
message += "." + arguments[i];
}
message += '" translation]';
return message;
I18n.missingTranslation = function(scope, key) {
var message = '[' + this.currentLocale() + "." + scope;
if (key) { message += "." + key; }
return message + ']';
};
I18n.currentLocale = function() {

View file

@ -55,23 +55,13 @@
// Border radius
@mixin border-radius-all($radius) {
border-radius: $radius;
}
@mixin border-radius-top($radius) {
border-top-right-radius: $radius;
border-top-left-radius: $radius;
}
@mixin border-radius-bottom($radius) {
border-bottom-right-radius: $radius;
border-bottom-left-radius: $radius;
border-radius: $radius;
}
// Box shadow
@mixin box-shadow($shadow) {
box-shadow: $shadow;
box-shadow: $shadow;
}
// Linear gradient
@ -81,32 +71,6 @@
background-image: linear-gradient(to bottom, $start-color, $end-color);
}
// Background size
@mixin background-size($size) {
background-size: $size;
}
// Background clip
@mixin background-clip($clip) {
background-clip: $clip;
}
// Rotate
@mixin rotate($degrees) {
-webkit-transform: rotate($degrees);
transform: rotate($degrees);
}
// Scale
@mixin scale($ratio) {
-webkit-transform: scale($ratio);
transform: scale($ratio);
}
// Transition
@mixin transition($transition) {
@ -138,12 +102,6 @@
}
}
@mixin fade-soft($time: 1s) {
-webkit-transition: opacity $time ease-in-out;
-ms-transition: opacity $time ease-in-out;
transition: opacity $time ease-in-out;
}
@mixin visible {
opacity: 1;
visibility: visible;
@ -151,16 +109,6 @@
transition-delay: 0s;
}
// Decorations
// --------------------------------------------------
// Glow
@mixin glow($color) {
border: 1px solid $color;
box-shadow: 0 0 5px $color;
}
//
// --------------------------------------------------

View file

@ -309,7 +309,7 @@
border: 1px dashed $gray;
overflow: auto;
visibility: visible;
&.hidden {
width: 0;
visibility: hidden;
@ -432,8 +432,7 @@ div.ac-wrap {
}
#wmd-input, #wmd-preview {
box-sizing: border-box;
-moz-box-sizing: border-box;
@include box-sizing(border-box);
width: 100%;
height: 100%;
min-height: 100%;
@ -453,7 +452,7 @@ div.ac-wrap {
top: 0;
height: 100%;
min-height: 100%;
box-sizing: border-box;
@include box-sizing(border-box);
border: 0;
border-top: 36px solid transparent;
@include border-radius-all(0);
@ -461,8 +460,7 @@ div.ac-wrap {
}
.textarea-wrapper, .preview-wrapper {
position: relative;
box-sizing: border-box;
-moz-box-sizing: border-box;
@include box-sizing(border-box);
height: 100%;
min-height: 100%;
margin: 0;

View file

@ -39,7 +39,7 @@ h1 .topic-statuses .topic-status i {margin-right: 5px;}
margin-left: -8px;
}
.avoid-tab {
.avoid-tab {
padding-top: 25px;
.topic-meta-data-inside {margin-top: -30px;}
}
@ -131,7 +131,7 @@ nav.post-controls {
border: none;
margin-left: 3px;
transition: all linear 0.15s;
outline: none;
outline: none;
&:hover {
background: #eee;
color: #888;
@ -394,12 +394,12 @@ span.post-count {
#topic-title {
z-index: 1000;
padding: 14px 0 8px 0;
h1 {
line-height: 1.2em;
overflow: hidden;
h1 {
line-height: 1.2em;
overflow: hidden;
}
.topic-statuses {
margin-top: -2px;
margin-top: -2px;
.icon-pushpin {margin-top: -1px;}
}
.star {font-size: 20px; margin-top: 8px;}
@ -435,7 +435,7 @@ iframe {
}
.extra-info-wrapper {
float: left;
float: left;
width: 78%;
.topic-statuses {margin-right: 5px;}
}
@ -499,7 +499,7 @@ iframe {
.contents .cooked {
padding-right: 30px;
h1, h2, h3 {margin: 10px 0;}
ul, ol {margin: 15px 0;}
ul, ol {margin: 0 15px;}
li p {margin: 3px 0;}
}

View file

@ -11,13 +11,14 @@
}
.post-actions {
@include unselectable;
clear: both;
text-align: right;
.post-action {
.post-action {
display: inline-block;
margin-left: 10px;
margin-top: 10px;
}
}
}
.post-menu-area {
margin-bottom: 10px;

View file

@ -78,9 +78,7 @@
.btn {
width: 100%;
margin-bottom: 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
@include box-sizing(border-box);
}
}
h2 {

View file

@ -389,8 +389,7 @@ div.ac-wrap {
}
#wmd-input, #wmd-preview {
box-sizing: border-box;
-moz-box-sizing: border-box;
@include box-sizing(border-box);
width: 100%;
height: 100%;
min-height: 100%;
@ -410,7 +409,7 @@ div.ac-wrap {
top: 0;
height: 100%;
min-height: 100%;
box-sizing: border-box;
@include box-sizing(border-box);
border: 0;
border-top: 36px solid transparent;
@include border-radius-all(0);
@ -418,8 +417,7 @@ div.ac-wrap {
}
.textarea-wrapper, .preview-wrapper {
position: relative;
box-sizing: border-box;
-moz-box-sizing: border-box;
@include box-sizing(border-box);
height: 100%;
min-height: 100%;
margin: 0;

View file

@ -148,8 +148,8 @@ $caption-subtitle-color: #BDBDBD !default;
top: 0;
padding: 0 $popup-padding-left;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
// Vertical centerer helper
@ -454,8 +454,8 @@ button::-moz-focus-inner {
display: block;
line-height: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: $image-padding-top 0 $image-padding-bottom;
margin: 0 auto;
}
@ -535,8 +535,8 @@ button::-moz-focus-inner {
padding: 3px 5px;
position: fixed;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.mfp-img-mobile .mfp-bottom-bar:empty {
padding: 0;

View file

@ -73,9 +73,7 @@
.btn {
width: 100%;
margin-bottom: 5px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
@include box-sizing(border-box);
}
}
h2 {

View file

@ -62,16 +62,12 @@ class Users::OmniauthCallbacksController < ApplicationController
BUILTIN_AUTH.each do |authenticator|
if authenticator.name == name
raise Discourse::InvalidAccess.new("provider is not enabled") unless SiteSetting.send("enable_#{name}_logins?")
return authenticator
end
end
Discourse.auth_providers.each do |provider|
if provider.name == name
return provider.authenticator
end
return provider.authenticator if provider.name == name
end
raise Discourse::InvalidAccess.new("provider is not found")

View file

@ -270,6 +270,7 @@ class SiteSetting < ActiveRecord::Base
# hidden setting only used by system
setting(:uncategorized_category_id, -1, hidden: true)
client_setting(:display_name_on_posts, false)
client_setting(:enable_names, true)
def self.call_discourse_hub?

View file

@ -7,23 +7,16 @@
'defer/google_diff_match_patch': <%= asset_path('defer/google_diff_match_patch.js').inspect.html_safe %>
};
var assetPath = function(asset){
return map[asset];
};
return assetPath;
return function(asset){ return map[asset]; };
})();
</script>
<script>
Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>';
Discourse.BaseUrl = '<%= RailsMultisite::ConnectionManagement.current_hostname %>';
Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>';
Discourse.BaseUrl = '<%= RailsMultisite::ConnectionManagement.current_hostname %>';
Discourse.BaseUri = '<%= Discourse::base_uri "/" %>';
Discourse.Environment = '<%= Rails.env %>';
Discourse.SiteSettings = PreloadStore.get('siteSettings');
Discourse.Router.map(function() {
Discourse.routeBuilder.call(this);
});
Discourse.Router.map(function() { Discourse.routeBuilder.call(this); });
Discourse.start()
</script>

View file

@ -11,6 +11,7 @@
<link rel="icon" type="image/png" href="<%=SiteSetting.favicon_url%>">
<link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>">
<%= javascript_include_tag "preload_store" %>
<%= javascript_include_tag "locales/#{I18n.locale}" %>

View file

@ -85,6 +85,8 @@ Discourse::Application.configure do
# a comma delimited list of emails your devs have
# developers have god like rights and may impersonate anyone in the system
# normal admins may only impersonate other moderators (not admins)
config.developer_emails = []
if emails = ENV["DEVELOPER_EMAILS"]
config.developer_emails = emails.split(",")
end
end

View file

@ -1062,6 +1062,7 @@ cs:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "zadejte text pro filtrování..."
admin:
title: 'Discourse Administrace'

View file

@ -683,6 +683,7 @@ da:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "type to filter..."
admin:
title: 'Discourse Admin'

View file

@ -1049,6 +1049,7 @@ de:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "Tippe etwas ein, um zu filtern..."
admin:
title: 'Discourse Administrator'

View file

@ -1074,7 +1074,7 @@ en:
# This section is exported to the javascript for i18n in the admin section
admin_js:
filter: "filter"
type_to_filter: "type to filter..."
admin:
title: 'Discourse Admin'

View file

@ -793,6 +793,7 @@ es:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "type to filter..."
admin:
title: 'Administrador'

View file

@ -1032,6 +1032,7 @@ fr:
create_post: "Répondre / Voir"
readonly: "Voir"
admin_js:
type_to_filter: "Commencez à taper pour filtrer..."
admin:
title: 'Administation Discourse'
moderator: 'Modérateur'

View file

@ -638,6 +638,7 @@ id:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "type to filter..."
admin:
title: 'Discourse Admin'

View file

@ -1001,6 +1001,7 @@ it:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "scrivi per filtrare..."
admin:
title: 'Amministrazione Discourse'

View file

@ -841,6 +841,7 @@ ko:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "필터를 입력하세요"
admin:
title: 'Discourse 관리자'

View file

@ -914,6 +914,7 @@ nb_NO:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "skriv for å filtrere..."
admin:
title: 'Discourse Admin'

View file

@ -1014,6 +1014,7 @@ nl:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: typ om te filteren...
admin:
title: Discourse Beheer

View file

@ -969,6 +969,7 @@ pseudo:
ƀřóŵšéř íš ťóó ółď ťó ŵóřǩ óɳ ťĥíš Ďíščóůřšé ƒóřůɱ</á>. Рłéášé <á ĥřéƒ="ĥťťƿ://ƀřóŵšéĥáƿƿý.čóɱ">ůƿǧřáďé
ýóůř ƀřóŵšéř</á>. ]]'
admin_js:
type_to_filter: '[[ ťýƿé ťó ƒíłťéř... ]]'
admin:
title: '[[ Ďíščóůřšé Áďɱíɳ ]]'
moderator: '[[ Ϻóďéřáťóř ]]'

View file

@ -602,6 +602,7 @@ pt:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "escreve para filtrar..."
admin:
title: 'Discourse Admin'

View file

@ -1069,6 +1069,7 @@ pt_BR:
# Essa seção é para o javascript para i18n no admin
admin_js:
type_to_filter: "escreva para filtrar..."
admin:
title: 'Discourse Admin'

View file

@ -1085,6 +1085,7 @@ ru:
create_post: 'Отвечать / Просматривать'
readonly: Просматривать
admin_js:
type_to_filter: 'Введите текст для фильтрации...'
admin:
title: 'Discourse Admin'
moderator: Модератор

View file

@ -788,6 +788,7 @@ sv:
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "skriv för att filtrera..."
admin:
title: 'Discourse Admin'

View file

@ -657,8 +657,7 @@ zh_CN:
auto_close_notice: "本主题将在%{timeLeft}后自动关闭"
auto_close_title: '自动关闭设置'
auto_close_save: "保存"
auto_close_cancel: "取消"
auto_close_remove: "不自动关闭该主题"
auto_close_remove: "不要自动关闭该主题"
progress:
title: 主题进度
@ -1075,9 +1074,10 @@ zh_CN:
# This section is exported to the javascript for i18n in the admin section
admin_js:
filter: "过滤器"
admin:
title: '论道 管理'
title: 'Discourse管理'
moderator: '版主'
dashboard:
@ -1259,6 +1259,8 @@ zh_CN:
change_site_setting: "更改站点设置"
change_site_customization: "更改站点自定义"
delete_site_customization: "删除站点自定义"
ban_user: "禁止用户"
unban_user: "解禁用户"
screened_emails:
title: "被屏蔽的邮件地址"
description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。"
@ -1330,7 +1332,11 @@ zh_CN:
user:
ban_failed: "禁止此用户时发生了错误 {{error}}"
unban_failed: "解禁此用户时发生了错误 {{error}}"
ban_duration: "你计划禁止该用户多久?(天)"
ban_duration: "你计划禁止该用户多久?"
ban_duration_units: "(天)"
ban_reason_label: "为什么禁止该用户?当其尝试登入时,会看到这条理由。"
ban_reason: "禁止的理由"
banned_by: "禁止操作者:"
delete_all_posts: "删除所有帖子"
delete_all_posts_confirm: "你将删除 %{posts} 个帖子和 %{topics} 个主题,确认吗?"
ban: "禁止"
@ -1387,7 +1393,8 @@ zh_CN:
deactivate_explanation: "已停用的用户必须重新验证他们的电子邮件。"
banned_explanation: "被禁止的用户无法登录。"
block_explanation: "被封禁的用户不能发表主题或者评论。"
trust_level_change_failed: "改变用户等级时出现了一个问题,"
trust_level_change_failed: "改变用户等级时出现了一个问题。"
ban_modal_title: "禁止用户"
site_content:
none: "选择内容类型以开始编辑。"

View file

@ -968,6 +968,7 @@ zh_TW:
readonly: "觀看"
# This section is exported to the javascript for i18n in the admin section
admin_js:
type_to_filter: "輸入過濾條件……"
admin:
title: '論道 管理'

View file

@ -722,6 +722,7 @@ en:
dominating_topic_minimum_percent: "What percentage of posts a user has to make in a topic before we consider it dominating."
enable_names: "Allow users to show their full names"
display_name_on_posts: "Also show a user's full name on their posts"
notification_types:
mentioned: "%{display_username} mentioned you in %{link}"

View file

@ -2,10 +2,9 @@ require_dependency "auth/current_user_provider"
class Auth::DefaultCurrentUserProvider
CURRENT_USER_KEY = "_DISCOURSE_CURRENT_USER"
API_KEY = "_DISCOURSE_API"
TOKEN_COOKIE = "_t"
CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
API_KEY ||= "_DISCOURSE_API"
TOKEN_COOKIE ||= "_t"
# do all current user initialization here
def initialize(env)
@ -64,9 +63,19 @@ class Auth::DefaultCurrentUserProvider
user.save!
end
cookies.permanent[TOKEN_COOKIE] = { value: user.auth_token, httponly: true }
make_developer_admin(user)
@env[CURRENT_USER_KEY] = user
end
def make_developer_admin(user)
if user.active? &&
!user.admin &&
Rails.configuration.respond_to?(:developer_emails) &&
Rails.configuration.developer_emails.include?(user.email)
user.update_column(:admin, true)
end
end
def log_off_user(session, cookies)
cookies[TOKEN_COOKIE] = nil
end

View file

@ -1,22 +1,36 @@
module Autospec
class BaseRunner
def run(args, specs)
end
def abort
end
def reload
# used when starting the runner - preloading happens here
def start(opts = {})
end
# indicates whether tests are running
def running?
true
end
def start
# launch a batch of specs/tests
def run(specs)
end
# used when we need to reload the whole application
def reload
end
# used to abort the current run
def abort
end
def failed_specs
[]
end
# used to stop the runner
def stop
end
end
end

View file

@ -1,39 +1,46 @@
require "rspec/core/formatters/base_formatter"
require "rspec/core/formatters/base_text_formatter"
module Autospec; end
class Autospec::Formatter < RSpec::Core::Formatters::BaseFormatter
def dump_summary(duration, total, failures, pending)
# failed_specs = examples.delete_if{|e| e.execution_result[:status] != "failed"}.map{|s| s.metadata[:location]}
class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
# # if this fails don't kill everything
# begin
# FileUtils.mkdir_p('tmp')
# File.open("./tmp/rspec_result","w") do |f|
# f.puts failed_specs.join("\n")
# end
# rescue
# # nothing really we can do, at least don't kill the test runner
# end
RSPEC_RESULT = "./tmp/rspec_result"
def initialize(output)
super
FileUtils.mkdir_p("tmp") unless Dir.exists?("tmp")
end
def start(count)
FileUtils.mkdir_p('tmp')
@fail_file = File.open("./tmp/rspec_result","w")
super(count)
def start(example_count)
super
File.delete(RSPEC_RESULT) if File.exists?(RSPEC_RESULT)
@fail_file = File.open(RSPEC_RESULT,"w")
end
def example_passed(example)
super
output.print success_color(".")
end
def example_pending(example)
super
output.print pending_color("*")
end
def example_failed(example)
super
output.print failure_color("F")
@fail_file.puts(example.metadata[:location] + " ")
@fail_file.flush
end
def start_dump
super
output.puts
end
def close
@fail_file.close
super
end
def example_failed(example)
@fail_file.puts example.metadata[:location]
@fail_file.flush
super(example)
end
end

241
lib/autospec/manager.rb Normal file
View file

@ -0,0 +1,241 @@
require "listen"
require "thread"
require "fileutils"
require "autospec/reload_css"
require "autospec/base_runner"
module Autospec; end
class Autospec::Manager
def self.run(opts={})
self.new.run(opts)
end
def initialize
@queue = []
@mutex = Mutex.new
@signal = ConditionVariable.new
end
def run(opts = {})
@runners = [ruby_runner, javascript_runner]
Signal.trap("HUP") { stop_runners; exit }
Signal.trap("INT") { stop_runners; exit }
ensure_all_specs_will_run
start_runners
start_service_queue
listen_for_changes
puts "Press [ENTER] to stop the current run"
while @runners.any?(&:running?)
STDIN.gets
process_queue
end
rescue => e
fail(e, "failed in run")
ensure
stop_runners
end
private
def ruby_runner
if ENV["SPORK"]
require "autospec/spork_runner"
Autospec::SporkRunner.new
else
require "autospec/simple_runner"
Autospec::SimpleRunner.new
end
end
def javascript_runner
require "autospec/qunit_runner"
Autospec::QunitRunner.new
end
def ensure_all_specs_will_run
@runners.each do |runner|
@queue << ['spec', 'spec', runner] unless @queue.any? { |f, s, r| s == "spec" && r == runner }
end
end
[:start, :stop, :abort].each do |verb|
define_method("#{verb}_runners") do
@runners.each(&verb)
end
end
def start_service_queue
Thread.new do
while true
thread_loop
end
end
end
# the main loop, will run the specs in the queue till one fails or the queue is empty
def thread_loop
@mutex.synchronize do
current = @queue.first
last_failed = false
last_failed = process_spec(current) if current
# stop & wait for the queue to have at least one item or when there's been a failure
@signal.wait(@mutex) if @queue.length == 0 || last_failed
end
rescue => e
fail(e, "failed in main loop")
end
# will actually run the spec and check whether the spec has failed or not
def process_spec(current)
has_failed = false
# retrieve the instance of the runner
runner = current[2]
# actually run the spec (blocking call)
result = runner.run(current[1]).to_i
if result == 0
# remove the spec from the queue
@queue.shift
else
has_failed = true
if result > 0
focus_on_failed_tests(current)
ensure_all_specs_will_run
end
end
has_failed
end
def focus_on_failed_tests(current)
runner = current[2]
# we only want 1 focus in the queue
@queue.shift if current[0] == "focus"
# focus on the first 10 failed specs
failed_specs = runner.failed_specs[0..10]
# focus on the failed specs
@queue.unshift ["focus", failed_specs.join(" "), runner] if failed_specs.length > 0
end
def listen_for_changes(opts = {})
options = {
ignore: /^public|^lib\/autospec/,
relative_paths: true,
}
if opts[:force_polling]
options[:force_polling] = true
options[:latency] = opts[:latency] || 3
end
Thread.start do
Listen.to('.', options) do |modified, added, removed|
process_change([modified, added].flatten.compact)
end
end
end
def process_change(files)
return if files.length == 0
specs = []
hit = false
files.each do |file|
@runners.each do |runner|
# reloaders
runner.reloaders.each do |k|
if k.match(file)
runner.reload
return
end
end
# watchers
runner.watchers.each do |k,v|
if m = k.match(file)
hit = true
spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file
specs << [file, spec, runner] if File.exists?(spec) || Dir.exists?(spec)
end
end
end
# special watcher for styles/templates
Autospec::ReloadCss::WATCHERS.each do |k,v|
matches = []
matches << file if k.match(file)
Autospec::ReloadCss.run_on_change(matches) if matches.present?
end
end
queue_specs(specs) if hit
rescue => e
fail(e, "failed in watcher")
end
def queue_specs(specs)
if specs.length == 0
locked = @mutex.try_lock
if locked
@signal.signal
@mutex.unlock
end
return
else
abort_runners
end
@mutex.synchronize do
specs.each do |file, spec, runner|
# make sure there's no other instance of this spec in the queue
@queue.delete_if { |f, s, r| s.strip == spec.strip && r == runner }
# deal with focused specs
if @queue.first && @queue.first[0] == "focus"
focus = @queue.shift
@queue.unshift([file, spec, runner])
if focus[1].include?(spec) || file != spec
@queue.unshift(focus)
end
else
@queue.unshift([file, spec, runner])
end
end
@signal.signal
end
end
def process_queue
if @queue.length == 0
ensure_all_specs_will_run
@signal.signal
else
current = @queue.first
runner = current[2]
specs = runner.failed_specs
puts
puts
if specs.length == 0
puts "No specs have failed yet!"
puts
else
puts "The following specs have failed:"
specs.each { |s| puts s }
puts
specs = specs.map { |s| [s, s, runner] }
queue_specs(specs)
end
end
end
def fail(exception, message = nil)
puts message if message
puts exception.message
puts exception.backtrace.join("\n")
end
end

View file

@ -0,0 +1,150 @@
require "demon/rails_autospec"
module Autospec
class QunitRunner < BaseRunner
WATCHERS = {}
def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end
def watchers; WATCHERS; end
# Discourse specific
watch(%r{^app/assets/javascripts/discourse/(.+)\.js$}) { |m| "test/javascripts/#{m[1]}_test.js" }
watch(%r{^app/assets/javascripts/admin/(.+)\.js$}) { |m| "test/javascripts/admin/#{m[1]}_test.js" }
watch(%r{^test/javascripts/.+\.js$})
RELOADERS = Set.new
def self.reload(pattern); RELOADERS << pattern; end
def reloaders; RELOADERS; end
# Discourse specific
reload(%r{^test/javascripts/fixtures/.+_fixtures\.js$})
reload(%r{^test/javascripts/(helpers|mixins)/.+\.js$})
reload("test/javascripts/test_helper.js")
require "socket"
class PhantomJsNotInstalled < Exception; end
def initialize
ensure_phantomjs_is_installed
end
def start
# ensure we can launch the rails server
unless port_available?(port)
puts "Port #{port} is not available"
puts "Either kill the process using that port or use the `TEST_SERVER_PORT` environment variable"
return
end
# start rails
start_rails_server
@running = true
end
def running?
@running
end
def run(specs)
puts "Running Qunit: #{specs}"
abort
qunit_url = "http://localhost:#{port}/qunit"
if specs != "spec" && specs.split.length == 1
module_name = try_to_find_module_name(specs.strip)
qunit_url << "?module=#{module_name}" if module_name
end
cmd = "phantomjs #{Rails.root}/lib/autospec/run-qunit.js \"#{qunit_url}\""
@pid = Process.spawn(cmd)
_, status = Process.wait2(@pid)
status.exitstatus
end
def reload
stop_rails_server
sleep 1
start_rails_server
end
def abort
if @pid
children_processes(@pid).each { |pid| kill_process(pid) }
kill_process(@pid)
@pid = nil
end
end
def failed_specs
specs = []
path = './tmp/qunit_result'
specs = File.readlines(path) if File.exist?(path)
specs
end
def stop
# kill phantomjs first
abort
stop_rails_server
@running = false
end
private
def ensure_phantomjs_is_installed
raise PhantomJsNotInstalled.new unless system("command -v phantomjs >/dev/null;")
end
def port_available?(port)
TCPServer.open(port).close
true
rescue Errno::EADDRINUSE
false
end
def port
@port ||= ENV["TEST_SERVER_PORT"] || 60099
end
def start_rails_server
Demon::RailsAutospec.start(1)
end
def stop_rails_server
Demon::RailsAutospec.stop
end
def children_processes(base = Process.pid)
process_tree = Hash.new { |hash, key| hash[key] = [key] }
Hash[*`ps -eo pid,ppid`.scan(/\d+/).map(&:to_i)].each do |pid, ppid|
process_tree[ppid] << process_tree[pid]
end
process_tree[base].flatten - [base]
end
def kill_process(pid)
return unless pid
Process.kill("INT", pid) rescue nil
while (Process.getpgid(pid) rescue nil)
sleep 0.001
end
end
def try_to_find_module_name(file)
return unless File.exists?(file)
File.open(file, "r").each_line do |line|
if m = /module\(['"]([^'"]+)/i.match(line)
return m[1]
end
end
end
end
end

View file

@ -1,23 +1,23 @@
module Autospec; end
class Autospec::ReloadCss
MATCHERS = {}
WATCHERS = {}
def self.watch(pattern, &blk)
MATCHERS[pattern] = blk
WATCHERS[pattern] = blk
end
watch(/tmp\/refresh_browser/)
# css, scss, sass or handlebars
watch(/\.css$/)
watch(/\.css\.erb$/)
watch(/\.sass$/)
watch(/\.scss$/)
watch(/\.sass\.erb$/)
watch(/\.ca?ss\.erb$/)
watch(/\.s[ac]ss$/)
watch(/\.handlebars$/)
def self.message_bus
MessageBus::Instance.new.tap do |bus|
bus.site_id_lookup do
# this is going to be dev the majority of the time, if you have multisite configured in dev stuff may be different
# this is going to be dev the majority of the time
# if you have multisite configured in dev stuff may be different
"default"
end
end
@ -26,13 +26,13 @@ class Autospec::ReloadCss
def self.run_on_change(paths)
paths.map! do |p|
hash = nil
fullpath = Rails.root.to_s + "/" + p
hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists? fullpath
fullpath = "#{Rails.root}/#{p}"
hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists?(fullpath)
p = p.sub /\.sass\.erb/, ""
p = p.sub /\.sass/, ""
p = p.sub /\.scss/, ""
p = p.sub /^app\/assets\/stylesheets/, "assets"
{name: p, hash: hash}
{ name: p, hash: hash }
end
message_bus.publish "/file-change", paths
end

View file

@ -0,0 +1,43 @@
module Autospec
class RspecRunner < BaseRunner
WATCHERS = {}
def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end
def watchers; WATCHERS; end
# Discourse specific
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
# Rails example
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^spec/support/.+\.rb$}) { "spec" }
watch("app/controllers/application_controller.rb") { "spec/controllers" }
# Capybara request specs
watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# Fabrication
watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" }
RELOADERS = Set.new
def self.reload(pattern); RELOADERS << pattern; end
def reloaders; RELOADERS; end
# We need to reload the whole app when changing any of these files
reload('spec/spec_helper.rb')
reload('config/(.*).rb')
reload('app/helpers/(.*).rb')
def failed_specs
specs = []
path = './tmp/rspec_result'
specs = File.readlines(path) if File.exist?(path)
specs
end
end
end

175
lib/autospec/run-qunit.js Normal file
View file

@ -0,0 +1,175 @@
// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC
if (phantom.args.length != 1) {
console.log("Usage: " + phantom.scriptName + " <URL>");
phantom.exit(1);
}
var system = require('system'),
fs = require('fs'),
page = require('webpage').create(),
QUNIT_RESULT = "./tmp/qunit_result";
if (fs.exists(QUNIT_RESULT) && fs.isFile(QUNIT_RESULT)) { fs.remove(QUNIT_RESULT); }
page.onConsoleMessage = function (message) {
// filter out Ember's debug messages
if (message.slice(0, 8) === "WARNING:") { return; }
if (message.slice(0, 6) === "DEBUG:") { return; }
console.log(message);
};
page.onCallback = function (message) {
// write to the result file
if (message.slice(0, 5) === "FILE:") { fs.write(QUNIT_RESULT, message.slice(6), "a"); }
// forward the message to the standard output
if (message.slice(0, 6) === "PRINT:") { system.stdout.write(message.slice(7)); }
};
page.start = new Date();
// -----------------------------------WARNING --------------------------------------
// calling "console.log" BELOW this line will go through the "page.onConsoleMessage"
// -----------------------------------WARNING --------------------------------------
page.open(phantom.args[0], function (status) {
if (status !== "success") {
console.log("\nNO NETWORK :(\n");
phantom.exit(1);
} else {
console.log("QUnit loaded in " + (new Date() - page.start) + " ms");
page.evaluate(colorizer);
page.evaluate(logQUnit);
// wait up to 60 seconds for QUnit to finish
var timeout = 60 * 1000,
start = Date.now();
var interval = setInterval(function() {
if (Date.now() - start > timeout) {
console.error("\nTIME OUT :(\n");
phantom.exit(1);
} else {
var qunitResult = page.evaluate(function() { return window.qunitResult; });
if (qunitResult) {
clearInterval(interval);
if (qunitResult.failed > 0) {
phantom.exit(1);
} else {
phantom.exit(0);
}
}
}
}, 250);
}
});
// https://github.com/jquery/qunit/pull/470
function colorizer() {
window.ANSI = {
colorMap: {
"red": "\u001b[31m",
"green": "\u001b[32m",
"blue": "\u001b[34m",
"end": "\u001b[0m"
},
highlightMap: {
"red": "\u001b[41m\u001b[37m", // change 37 to 30 for black text
"green": "\u001b[42m\u001b[30m",
"blue": "\u001b[44m\u001b[37m",
"end": "\u001b[0m"
},
highlight: function (text, color) {
var colorCode = this.highlightMap[color],
colorEnd = this.highlightMap.end;
return colorCode + text + colorEnd;
},
colorize: function (text, color) {
var colorCode = this.colorMap[color],
colorEnd = this.colorMap.end;
return colorCode + text + colorEnd;
}
};
};
function logQUnit() {
// keep track of error messages
var errors = {};
QUnit.begin(function () {
console.log("BEGIN");
});
QUnit.log(function (context) {
if (!context.result) {
var module = context.module,
test = context.name;
var assertion = {
message: context.message,
expected: context.expected,
actual: context.actual
};
if (!errors[module]) { errors[module] = {}; }
if (!errors[module][test]) { errors[module][test] = []; }
errors[module][test].push(assertion);
var fileName = context.source
.replace(/[^\S\n]+at[^\S\n]+/g, "")
.split("\n")[1]
.replace(/\?.+$/, "")
.replace(/^.+\/assets\//, "test/javascripts/");
window.callPhantom("FILE: " + fileName + " ");
}
});
QUnit.testDone(function (context) {
if (context.failed > 0) {
window.callPhantom("PRINT: " + ANSI.colorize("F", "red"));
} else {
window.callPhantom("PRINT: " + ANSI.colorize(".", "green"));
}
});
QUnit.done(function (context) {
console.log("\n");
// display failures
if (Object.keys(errors).length > 0) {
console.log("Failures:\n");
for (m in errors) {
var module = errors[m];
console.log("Module Failed: " + ANSI.highlight(m, "red"));
for (t in module) {
var test = module[t];
console.log(" Test Failed: " + t);
for (var a = 0; a < test.length; a++) {
var assertion = test[a];
console.log(" Assertion Failed: " + (assertion.message || ""));
if (assertion.expected) {
console.log(" Expected: " + assertion.expected);
console.log(" Actual: " + assertion.actual);
}
}
}
}
}
// display summary
console.log("\n");
console.log("Finished in " + (context.runtime / 1000) + " seconds");
var color = context.failed > 0 ? "red" : "green";
console.log(ANSI.colorize(context.total + " examples, " + context.failed + " failures", color));
// we're done
window.qunitResult = context;
});
};

View file

@ -1,308 +0,0 @@
require "drb/drb"
require "thread"
require "fileutils"
require "autospec/reload_css"
require "autospec/base_runner"
require "autospec/simple_runner"
require "autospec/spork_runner"
module Autospec; end
class Autospec::Runner
MATCHERS = {}
def self.watch(pattern, &blk)
MATCHERS[pattern] = blk
end
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
# Rails example
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" }
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
watch("app/controllers/application_controller.rb") { "spec/controllers" }
# Capybara request specs
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# Fabrication
watch(%r{^spec/fabricators/(.+)_fabricator\.rb$}) { "spec" }
RELOAD_MATCHERS = Set.new
def self.watch_reload(pattern)
RELOAD_MATCHERS << pattern
end
watch_reload('spec/spec_helper.rb')
watch_reload('config/(.*).rb')
watch_reload(%r{app/helpers/(.*).rb})
def self.run(opts={})
self.new.run(opts)
end
def initialize
@queue = []
@mutex = Mutex.new
@signal = ConditionVariable.new
start_service_queue
end
def run(opts = {})
puts "Forced polling (slower) - inotify does not work on network filesystems, use local filesystem to avoid" if opts[:force_polling]
if ENV["SPORK"] == "0"
puts "Using Simple Runner"
@runner = Autospec::SimpleRunner.new
else
puts "Using Spork Runner"
@runner = Autospec::SporkRunner.new
end
@runner.start
Signal.trap("HUP") {@runner.stop; exit }
Signal.trap("SIGINT") {@runner.stop; exit }
options = {filter: /^app|^spec|^lib/, relative_paths: true}
if opts[:force_polling]
options[:force_polling] = true
options[:latency] = opts[:latency] || 3
end
Thread.start do
Listen.to('.', options ) do |modified, added, removed|
process_change([modified, added].flatten.compact)
end
end
@mutex.synchronize do
@queue << ['spec', 'spec']
@signal.signal
end
while @runner.running?
process_queue
end
rescue => e
puts e
puts e.backtrace
@runner.stop
end
def process_queue
STDIN.gets
if @queue.length == 0
@queue << ['spec', 'spec']
@signal.signal
else
specs = failed_specs(:delete => false)
puts
puts
if specs.length == 0
puts "No specs have failed yet!"
puts
else
puts "The following specs have failed: "
specs.each do |s|
puts s
end
puts
queue_specs(specs.zip specs)
end
end
end
def wait_for(timeout_milliseconds)
timeout = (timeout_milliseconds + 0.0) / 1000
finish = Time.now + timeout
t = Thread.new do
while Time.now < finish && !yield
sleep(0.001)
end
end
t.join rescue nil
end
def force_polling?
works = false
begin
require 'rb-inotify'
require 'fileutils'
n = INotify::Notifier.new
FileUtils.touch('./tmp/test_polling')
n.watch("./tmp", :modify, :attrib){ works = true }
quit = false
Thread.new do
while !works && !quit
if IO.select([n.to_io], [], [], 0.1)
n.process
end
end
end
sleep 0.01
FileUtils.touch('./tmp/test_polling')
wait_for(100) { works }
File.unlink('./tmp/test_polling')
n.stop
quit = true
rescue LoadError
#assume it works (mac)
works = true
end
!works
end
def process_change(files)
return unless files.length > 0
specs = []
hit = false
files.each do |file|
RELOAD_MATCHERS.each do |k|
if k.match(file)
@runner.reload
return
end
end
MATCHERS.each do |k,v|
if m = k.match(file)
hit = true
spec = v ? ( v.arity == 1 ? v.call(m) : v.call ) : file
if File.exists?(spec) || Dir.exists?(spec)
specs << [file, spec]
end
end
end
Autospec::ReloadCss::MATCHERS.each do |k,v|
matches = []
if k.match(file)
matches << file
end
Autospec::ReloadCss.run_on_change(matches) if matches.present?
end
end
queue_specs(specs) if hit
rescue => e
p "failed in watcher"
p e
p e.backtrace
end
def queue_specs(specs)
if specs.length == 0
locked = @mutex.try_lock
if locked
@signal.signal
@mutex.unlock
end
return
else
@runner.abort
end
@mutex.synchronize do
specs.each do |c,spec|
@queue.delete([c,spec])
if @queue.last && @queue.last[0] == "focus"
focus = @queue.pop
@queue << [c,spec]
if focus[1].include?(spec) || c != spec
@queue << focus
end
else
@queue << [c,spec]
end
end
@signal.signal
end
end
def thread_loop
@mutex.synchronize do
last_failed = false
current = @queue.last
if current
last_failed = process_spec(current[1])
end
wait = @queue.length == 0 || last_failed
@signal.wait(@mutex) if wait
end
rescue => e
p "DISASTA PASTA"
puts e
puts e.backtrace
end
def process_spec(spec)
last_failed = false
result = run_spec(spec)
if result == 0
@queue.pop
else
last_failed = true
if result.to_i > 0
focus_on_failed_tests
ensure_all_specs_will_run
end
end
last_failed
end
def start_service_queue
@worker ||= Thread.new do
while true
thread_loop
end
end
end
def focus_on_failed_tests
current = @queue.last
specs = failed_specs[0..10]
if current[0] == "focus"
@queue.pop
end
@queue << ["focus", specs.join(" ")]
end
def ensure_all_specs_will_run
unless @queue.any?{|s,t| t == 'spec'}
@queue.unshift(['spec','spec'])
end
end
def failed_specs(opts={:delete => true})
specs = []
path = './tmp/rspec_result'
if File.exist?(path)
specs = File.open(path) { |file| file.read.split("\n") }
File.delete(path) if opts[:delete]
end
specs
end
def run_spec(specs)
File.delete("tmp/rspec_result") if File.exists?("tmp/rspec_result")
args = ["-f", "progress", specs.split(" "),
"-r", "#{File.dirname(__FILE__)}/formatter.rb",
"-f", "Autospec::Formatter"].flatten
@runner.run(args, specs)
end
end

View file

@ -1,26 +1,36 @@
require "autospec/rspec_runner"
module Autospec
class SimpleRunner < BaseRunner
class SimpleRunner < RspecRunner
def run(specs)
puts "Running Rspec: " << specs
# kill previous rspec instance
abort
# we use our custom rspec formatter
args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb",
"-f", "Autospec::Formatter", specs.split].flatten.join(" ")
# launch rspec
@pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec #{args}")
_, status = Process.wait2(@pid)
status.exitstatus
end
def abort
if @pid
Process.kill("SIGINT", @pid) rescue nil
while(Process.getpgid(@pid) rescue nil)
Process.kill("INT", @pid) rescue nil
while (Process.getpgid(@pid) rescue nil)
sleep 0.001
end
@pid = nil
end
end
def run(args, spec)
self.abort
puts "Running: " << spec
@pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec " << args.join(" "))
pid, status = Process.wait2(@pid)
status
def stop
abort
end
def stop
self.abort
end
end
end

View file

@ -1,5 +1,9 @@
require "drb/drb"
require "autospec/rspec_runner"
module Autospec
class SporkRunner < BaseRunner
class SporkRunner < RspecRunner
def start
if already_running?(pid_file)
@ -13,33 +17,38 @@ module Autospec
end
def running?
# launch a thread that will wait for spork to die
@monitor_thread ||=
Thread.new do
Process.wait(@spork_pid)
@spork_running = false
end
@spork_running
end
def stop
stop_spork
end
def run(args,specs)
def run(specs)
args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb",
"-f", "Autospec::Formatter", specs.split].flatten
spork_service.run(args,$stderr,$stdout)
end
def abort
spork_service.abort
end
def reload
stop_spork
sleep 1
start_spork
end
def abort
spork_service.abort
end
def stop
stop_spork
end
private
def spork_pid_file
Rails.root + "tmp/pids/spork.pid"
end
@ -55,7 +64,7 @@ module Autospec
end
end
def write_pid_file(file,pid)
def write_pid_file(file, pid)
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(file,'w') do |f|
f.write(pid)
@ -67,25 +76,18 @@ module Autospec
end
def spork_service
unless @drb_listener_running
begin
DRb.start_service("druby://127.0.0.1:0")
rescue SocketError, Errno::EADDRNOTAVAIL
DRb.start_service("druby://:0")
end
@drb_listener_running = true
end
@spork_service ||= DRbObject.new_with_uri("druby://127.0.0.1:8989")
end
def stop_spork
pid = File.read(spork_pid_file).to_i
Process.kill("SIGTERM",pid)
end
def start_spork
if already_running?(spork_pid_file)
puts "Killing old orphan spork instance"
@ -101,7 +103,13 @@ module Autospec
running = spork_running?
sleep 0.01
end
end
def stop_spork
pid = File.read(spork_pid_file).to_i
Process.kill("SIGTERM", pid) rescue nil
end
end
end

142
lib/demon/base.rb Normal file
View file

@ -0,0 +1,142 @@
module Demon; end
# intelligent fork based demonizer
class Demon::Base
def self.start(count)
@demons ||= {}
count.times do |i|
(@demons["#{prefix}_#{i}"] ||= new(i)).start
end
end
def self.stop
return unless @demons
@demons.values.each do |demon|
demon.stop
end
end
def initialize(index)
@index = index
@pid = nil
@parent_pid = Process.pid
@monitor = nil
end
def pid_file
"#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
end
def stop
if @monitor
@monitor.kill
@monitor.join
@monitor = nil
end
if @pid
Process.kill("HUP",@pid)
@pid = nil
end
end
def start
if existing = already_running?
# should not happen ... so kill violently
Process.kill("TERM",existing)
end
return if @pid
if @pid = fork
write_pid_file
monitor_child
return
end
monitor_parent
establish_app
after_fork
end
def already_running?
if File.exists? pid_file
pid = File.read(pid_file).to_i
if alive?(pid)
return pid
end
end
nil
end
private
def monitor_child
@monitor ||= Thread.new do
while true
sleep 5
unless alive?(@pid)
STDERR.puts "#{@pid} died, restarting the process"
@pid = nil
start
end
end
end
end
def write_pid_file
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(pid_file,'w') do |f|
f.write(@pid)
end
end
def delete_pid_file
File.delete(pid_file)
end
def monitor_parent
Thread.new do
while true
unless alive?(@parent_pid)
Process.kill "QUIT", Process.pid
end
sleep 1
end
end
end
def alive?(pid)
begin
Process.getpgid(pid)
true
rescue Errno::ESRCH
false
end
end
def establish_app
ActiveRecord::Base.connection_handler.clear_active_connections!
ActiveRecord::Base.establish_connection
$redis.client.reconnect
Rails.cache.reconnect
MessageBus.after_fork
Signal.trap("HUP") do
begin
delete_pid_file
ensure
exit
end
end
# keep stuff simple for now
$stdout.reopen("/dev/null", "w")
$stderr.reopen("/dev/null", "w")
end
def after_fork
end
end

View file

@ -0,0 +1,25 @@
require "demon/base"
class Demon::RailsAutospec < Demon::Base
def self.prefix
"rails-autospec"
end
private
def after_fork
require "rack"
ENV["RAILS_ENV"] = "test"
Rack::Server.start(
:config => "config.ru",
:AccessLog => [],
:Port => ENV["TEST_SERVER_PORT"] || 60099,
)
rescue => e
STDERR.puts e.message
STDERR.puts e.backtrace.join("\n")
exit 1
end
end

View file

@ -1,148 +1,7 @@
module Demon; end
# intelligent fork based demonizer for sidekiq
class Demon::Base
def self.start(count)
@demons ||= {}
count.times do |i|
(@demons["#{prefix}_#{i}"] ||= new(i)).start
end
end
def self.stop
@demons.values.each do |demon|
demon.stop
end
end
def initialize(index)
@index = index
@pid = nil
@parent_pid = Process.pid
@monitor = nil
end
def pid_file
"#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
end
def stop
if @monitor
@monitor.kill
@monitor.join
@monitor = nil
end
if @pid
Process.kill("SIGHUP",@pid)
@pid = nil
end
end
def start
if existing = already_running?
# should not happen ... so kill violently
Process.kill("SIGTERM",existing)
end
return if @pid
if @pid = fork
write_pid_file
monitor_child
return
end
monitor_parent
establish_app
after_fork
end
def already_running?
if File.exists? pid_file
pid = File.read(pid_file).to_i
if alive?(pid)
return pid
end
end
nil
end
private
def monitor_child
@monitor ||= Thread.new do
while true
sleep 5
unless alive?(@pid)
STDERR.puts "#{@pid} died, restarting sidekiq"
@pid = nil
start
end
end
end
end
def write_pid_file
FileUtils.mkdir_p(Rails.root + "tmp/pids")
File.open(pid_file,'w') do |f|
f.write(@pid)
end
end
def delete_pid_file
File.delete(pid_file)
end
def monitor_parent
Thread.new do
while true
unless alive?(@parent_pid)
Process.kill "QUIT", Process.pid
end
sleep 1
end
end
end
def alive?(pid)
begin
Process.getpgid(pid)
true
rescue Errno::ESRCH
false
end
end
def establish_app
ActiveRecord::Base.connection_handler.clear_active_connections!
ActiveRecord::Base.establish_connection
$redis.client.reconnect
Rails.cache.reconnect
MessageBus.after_fork
Signal.trap("HUP") do
begin
delete_pid_file
ensure
exit
end
end
# keep stuff simple for now
$stdout.reopen("/dev/null", "w")
# $stderr.reopen("/dev/null", "w")
end
def after_fork
end
end
require "demon/base"
class Demon::Sidekiq < Demon::Base
def self.prefix
"sidekiq"
end
@ -151,18 +10,15 @@ class Demon::Sidekiq < Demon::Base
def after_fork
require 'sidekiq/cli'
begin
# Reload initializer cause it needs to run after sidekiq/cli
# was required
load Rails.root + "config/initializers/sidekiq.rb"
cli = Sidekiq::CLI.instance
cli.parse([])
cli.run
rescue => e
STDERR.puts e.message
STDERR.puts e.backtrace.join("\n")
exit 1
end
# Reload initializer cause it needs to run after sidekiq/cli was required
load Rails.root + "config/initializers/sidekiq.rb"
cli = Sidekiq::CLI.instance
cli.parse([])
cli.run
rescue => e
STDERR.puts e.message
STDERR.puts e.backtrace.join("\n")
exit 1
end
end

20
lib/tasks/assets.rake Normal file
View file

@ -0,0 +1,20 @@
task 'assets:precompile' => 'environment' do
# see: https://github.com/rails/sprockets-rails/issues/49
# a decision was made no longer to copy non-digested assets
# this breaks stuff like the emoji plugin. We could fix it,
# but its a major pain with little benefit.
if rails4?
puts "Copying non-digested versions of assets"
assets = Dir.glob(File.join(Rails.root, 'public/assets/**/*'))
regex = /(-{1}[a-z0-9]{32}*\.{1}){1}/
assets.each do |file|
next if File.directory?(file) || file !~ regex
source = file.split('/')
source.push(source.pop.gsub(regex, '.'))
non_digested = File.join(source)
FileUtils.cp(file, non_digested)
end
end
end

View file

@ -4,22 +4,17 @@
desc "Run all specs automatically as needed"
task "autospec" => :environment do
require 'autospec/manager'
if RUBY_PLATFORM.include?('linux')
require 'rb-inotify'
end
require 'listen'
puts "If file watching is not working you can force polling with: bundle exec rake autospec p l=3"
require 'autospec/runner'
force_polling = ARGV.any?{|a| a == "p" || a == "polling"}
latency = ((ARGV.find{|a| a =~ /l=|latency=/}||"").split("=")[1] || 3).to_i
force_polling = ARGV.any?{ |a| a == "p" || a == "polling" }
latency = ((ARGV.find{ |a| a =~ /l=|latency=/ } || "").split("=")[1] || 3).to_i
if force_polling
puts "polling has been forced (slower) checking every #{latency} #{"second".pluralize(latency)}"
puts "Polling has been forced (slower) - checking every #{latency} #{"second".pluralize(latency)}"
else
puts "If file watching is not working, you can force polling with: bundle exec rake autospec p l=3"
end
Autospec::Runner.run(force_polling: force_polling, latency: latency)
Autospec::Manager.run(force_polling: force_polling, latency: latency)
end

View file

@ -42,6 +42,19 @@ class UserDestroyer
b.record_match! if b
end
Post.with_deleted.where(user_id: user.id).update_all("user_id = NULL")
# If this user created categories, fix those up:
categories = Category.where(user_id: user.id).all
categories.each do |c|
c.user_id = Discourse.system_user.id
c.save!
if topic = Topic.with_deleted.where(id: c.topic_id).first
topic.try(:recover!)
topic.user_id = Discourse.system_user.id
topic.save!
end
end
StaffActionLogger.new(@staff).log_user_deletion(user, opts.slice(:context))
DiscourseHub.unregister_nickname(user.username) if SiteSetting.call_discourse_hub?
MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id]

View file

@ -238,6 +238,17 @@ describe UserDestroyer do
UserDestroyer.new(@admin).destroy(@user, {block_ip: true})
end
end
context 'user created a category' do
let!(:category) { Fabricate(:category, user: @user) }
it "assigns the system user to the categories" do
UserDestroyer.new(@admin).destroy(@user, {delete_posts: true})
category.reload.user_id.should == Discourse.system_user.id
category.topic.should be_present
category.topic.user_id.should == Discourse.system_user.id
end
end
end
end

View file

@ -28,7 +28,6 @@ describe UserSerializer do
end
it "has a name" do
puts json[:name]
json[:name].should be_blank
end
end

View file

@ -8,27 +8,21 @@ if (args.length < 1 || args.length > 2) {
phantom.exit(1);
}
var fs = require('fs');
function print(str) {
fs.write('/dev/stdout', str, 'w');
}
var page = require('webpage').create();
var system = require("system"),
page = require('webpage').create();
page.onConsoleMessage = function(msg) {
if (msg.slice(0,8) === 'WARNING:') { return; }
if (msg.slice(0,6) === 'DEBUG:') { return; }
// Hack to access the print method
// If there's a better way to do this, please change
if (msg.slice(0,6) === 'PRINT:') {
print(msg.slice(7));
return;
}
console.log(msg);
};
page.onCallback = function (message) {
// forward the message to the standard output
system.stdout.write(message);
};
page.open(args[0], function(status) {
if (status !== 'success') {
console.error("Unable to access network");
@ -80,9 +74,9 @@ function logQUnit() {
var msg = " Test Failed: " + context.name + assertionErrors.join(" ");
testErrors.push(msg);
assertionErrors = [];
console.log('PRINT: F');
window.callPhantom('F');
} else {
console.log('PRINT: .');
window.callPhantom('.');
}
});